feat: revoke certificate #293

This commit is contained in:
Jacky 2025-04-10 16:15:07 +08:00
parent 52a23750b5
commit c073801794
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
35 changed files with 1785 additions and 550 deletions

View file

@ -79,6 +79,7 @@ func autoCert(certModel *model.Cert) {
NotBefore: certInfo.NotBefore,
MustStaple: certModel.MustStaple,
LegoDisableCNAMESupport: certModel.LegoDisableCNAMESupport,
RevokeOld: certModel.RevokeOld,
}
if certModel.Resource != nil {

View file

@ -8,6 +8,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/cert/dns"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/internal/transport"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/go-acme/lego/v4/challenge/dns01"
@ -162,6 +163,20 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
}()
}
// Backup current certificate and key if RevokeOld is true
var oldResource *model.CertificateResource
if payload.RevokeOld && payload.Resource != nil && payload.Resource.Certificate != nil {
l.Println("[INFO] [Nginx UI] Backing up current certificate for later revocation")
// Save a copy of the old certificate and key
oldResource = &model.CertificateResource{
Resource: payload.Resource.Resource,
Certificate: payload.Resource.Certificate,
PrivateKey: payload.Resource.PrivateKey,
}
}
if time.Now().Sub(payload.NotBefore).Hours()/24 <= 21 &&
payload.Resource != nil && payload.Resource.Certificate != nil {
renew(payload, client, l, errChan)
@ -180,6 +195,25 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
ReloadServerTLSCertificate()
}
// Revoke old certificate if requested and we have a backup
if payload.RevokeOld && oldResource != nil && len(oldResource.Certificate) > 0 {
l.Println("[INFO] [Nginx UI] Revoking old certificate")
// Create a payload for revocation using old certificate
revokePayload := &ConfigPayload{
CertID: payload.CertID,
ServerName: payload.ServerName,
ChallengeMethod: payload.ChallengeMethod,
DNSCredentialID: payload.DNSCredentialID,
ACMEUserID: payload.ACMEUserID,
KeyType: payload.KeyType,
Resource: oldResource,
}
// Revoke the old certificate
revoke(revokePayload, client, l, errChan)
}
// Wait log to be written
time.Sleep(2 * time.Second)
}

View file

@ -1,6 +1,12 @@
package cert
import (
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
@ -8,11 +14,6 @@ import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/pkg/errors"
"github.com/uozi-tech/cosy/logger"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type ConfigPayload struct {
@ -29,6 +30,7 @@ type ConfigPayload struct {
CertificateDir string `json:"-"`
SSLCertificatePath string `json:"-"`
SSLCertificateKeyPath string `json:"-"`
RevokeOld bool `json:"revoke_old"`
}
func (c *ConfigPayload) GetACMEUser() (user *model.AcmeUser, err error) {
@ -110,6 +112,7 @@ func (c *ConfigPayload) WriteFile(l *log.Logger, errChan chan error) {
db.Where("id = ?", c.CertID).Updates(&model.Cert{
SSLCertificatePath: c.GetCertificatePath(),
SSLCertificateKeyPath: c.GetCertificateKeyPath(),
Resource: c.Resource,
})
}

105
internal/cert/revoke.go Normal file
View file

@ -0,0 +1,105 @@
package cert
import (
"log"
"os"
"time"
"github.com/0xJacky/Nginx-UI/internal/transport"
"github.com/go-acme/lego/v4/lego"
legolog "github.com/go-acme/lego/v4/log"
"github.com/pkg/errors"
"github.com/uozi-tech/cosy/logger"
cSettings "github.com/uozi-tech/cosy/settings"
)
// RevokeCert revokes a certificate and provides log messages through channels
func RevokeCert(payload *ConfigPayload, logChan chan string, errChan chan error) {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
}
}()
// Initialize a channel writer to receive logs
cw := NewChannelWriter()
defer close(errChan)
defer close(cw.Ch)
// Initialize a logger
l := log.New(os.Stderr, "", log.LstdFlags)
l.SetOutput(cw)
// Hijack the logger of lego
oldLogger := legolog.Logger
legolog.Logger = l
// Restore the original logger
defer func() {
legolog.Logger = oldLogger
}()
// Start a goroutine to fetch and process logs from channel
go func() {
for msg := range cw.Ch {
logChan <- string(msg)
}
}()
// Create client for communication with CA server
l.Println("[INFO] [Nginx UI] Preparing for certificate revocation")
user, err := payload.GetACMEUser()
if err != nil {
errChan <- errors.Wrap(err, "get ACME user error")
return
}
config := lego.NewConfig(user)
config.CADirURL = user.CADir
// Skip TLS check if proxy is configured
if config.HTTPClient != nil {
t, err := transport.NewTransport(
transport.WithProxy(user.Proxy))
if err != nil {
errChan <- errors.Wrap(err, "create transport error")
return
}
config.HTTPClient.Transport = t
}
config.Certificate.KeyType = payload.GetKeyType()
// Create the client
client, err := lego.NewClient(config)
if err != nil {
errChan <- errors.Wrap(err, "create client error")
return
}
revoke(payload, client, l, errChan)
// If the revoked certificate was used for the server itself, reload server TLS certificate
if payload.GetCertificatePath() == cSettings.ServerSettings.SSLCert &&
payload.GetCertificateKeyPath() == cSettings.ServerSettings.SSLKey {
l.Println("[INFO] [Nginx UI] Certificate was used for server, reloading server TLS certificate")
ReloadServerTLSCertificate()
}
l.Println("[INFO] [Nginx UI] Revocation completed")
// Wait for logs to be written
time.Sleep(2 * time.Second)
}
// revoke implements the internal certificate revocation logic
func revoke(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
l.Println("[INFO] [Nginx UI] Revoking certificate")
err := client.Certificate.Revoke(payload.Resource.Certificate)
if err != nil {
errChan <- errors.Wrap(err, "revoke certificate error")
return
}
l.Println("[INFO] [Nginx UI] Certificate successfully revoked")
return
}