mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
refactor: certificates storage
This commit is contained in:
parent
bc56932417
commit
9560c583e9
12 changed files with 513 additions and 392 deletions
|
|
@ -1,354 +1,37 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/net/idna"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
const (
|
||||
baseCertificatesFolderName = "certificates"
|
||||
baseArchivesFolderName = "archives"
|
||||
)
|
||||
|
||||
const (
|
||||
issuerExt = ".issuer.crt"
|
||||
certExt = ".crt"
|
||||
keyExt = ".key"
|
||||
pemExt = ".pem"
|
||||
pfxExt = ".pfx"
|
||||
resourceExt = ".json"
|
||||
)
|
||||
|
||||
// CertificatesStorage a certificates' storage.
|
||||
//
|
||||
// rootPath:
|
||||
//
|
||||
// ./.lego/certificates/
|
||||
// │ └── root certificates directory
|
||||
// └── "path" option
|
||||
//
|
||||
// archivePath:
|
||||
//
|
||||
// ./.lego/archives/
|
||||
// │ └── archived certificates directory
|
||||
// └── "path" option
|
||||
type CertificatesStorage struct {
|
||||
rootPath string
|
||||
archivePath string
|
||||
pem bool
|
||||
pfx bool
|
||||
pfxPassword string
|
||||
pfxFormat string
|
||||
filename string // Deprecated
|
||||
*storage.CertificatesWriter
|
||||
*storage.CertificatesReader
|
||||
}
|
||||
|
||||
// NewCertificatesStorage create a new certificates storage.
|
||||
func NewCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
|
||||
pfxFormat := cmd.String(flgPFXFormat)
|
||||
// newCertificatesStorage create a new certificates storage.
|
||||
func newCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
|
||||
basePath := cmd.String(flgPath)
|
||||
|
||||
switch pfxFormat {
|
||||
case "DES", "RC2", "SHA256":
|
||||
default:
|
||||
log.Fatal("Invalid PFX format.", slog.String("format", pfxFormat))
|
||||
config := storage.CertificatesWriterConfig{
|
||||
BasePath: basePath,
|
||||
PEM: cmd.Bool(flgPEM),
|
||||
PFX: cmd.Bool(flgPFX),
|
||||
PFXFormat: cmd.String(flgPFXPass),
|
||||
PFXPassword: cmd.String(flgPFXFormat),
|
||||
Filename: cmd.String(flgFilename),
|
||||
}
|
||||
|
||||
writer, err := storage.NewCertificatesWriter(config)
|
||||
if err != nil {
|
||||
log.Fatal("Certificates storage initialization", log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
return &CertificatesStorage{
|
||||
rootPath: filepath.Join(cmd.String(flgPath), baseCertificatesFolderName),
|
||||
archivePath: filepath.Join(cmd.String(flgPath), baseArchivesFolderName),
|
||||
pem: cmd.Bool(flgPEM),
|
||||
pfx: cmd.Bool(flgPFX),
|
||||
pfxPassword: cmd.String(flgPFXPass),
|
||||
pfxFormat: pfxFormat,
|
||||
filename: cmd.String(flgFilename),
|
||||
CertificatesWriter: writer,
|
||||
CertificatesReader: storage.NewCertificatesReader(basePath),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) CreateRootFolder() {
|
||||
err := createNonExistingFolder(s.rootPath)
|
||||
if err != nil {
|
||||
log.Fatal("Could not check/create the root folder",
|
||||
slog.String("filepath", s.rootPath),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) CreateArchiveFolder() {
|
||||
err := createNonExistingFolder(s.archivePath)
|
||||
if err != nil {
|
||||
log.Fatal("Could not check/create the archive folder.",
|
||||
slog.String("filepath", s.archivePath),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) GetRootPath() string {
|
||||
return s.rootPath
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
||||
domain := certRes.Domain
|
||||
|
||||
// We store the certificate, private key and metadata in different files
|
||||
// as web servers would not be able to work with a combined file.
|
||||
err := s.WriteFile(domain, certExt, certRes.Certificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save Certificate.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
err = s.WriteFile(domain, issuerExt, certRes.IssuerCertificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save IssuerCertificate.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// if we were given a CSR, we don't know the private key
|
||||
if certRes.PrivateKey != nil {
|
||||
err = s.WriteCertificateFiles(domain, certRes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save PrivateKey.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
} else if s.pem || s.pfx {
|
||||
// we don't have the private key; can't write the .pem or .pfx file
|
||||
log.Fatal("Unable to save PEM or PFX without the private key. Are you using a CSR?", log.DomainAttr(domain))
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatal("Unable to marshal CertResource.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
err = s.WriteFile(domain, resourceExt, jsonBytes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save CertResource.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
|
||||
raw, err := s.ReadFile(domain, resourceExt)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the metadata.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
var resource certificate.Resource
|
||||
if err = json.Unmarshal(raw, &resource); err != nil {
|
||||
log.Fatal("Error while marshaling the metadata.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
|
||||
filePath := s.GetFileName(domain, extension)
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Fatal("File stat", slog.String("filepath", filePath), log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
|
||||
return os.ReadFile(s.GetFileName(domain, extension))
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
|
||||
filename := sanitizedDomain(domain) + extension
|
||||
return filepath.Join(s.rootPath, filename)
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
|
||||
content, err := s.ReadFile(domain, extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The input may be a bundle or a single certificate.
|
||||
return certcrypto.ParsePEMBundle(content)
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {
|
||||
var baseFileName string
|
||||
if s.filename != "" {
|
||||
baseFileName = s.filename
|
||||
} else {
|
||||
baseFileName = sanitizedDomain(domain)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(s.rootPath, baseFileName+extension)
|
||||
|
||||
return os.WriteFile(filePath, data, filePerm)
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *certificate.Resource) error {
|
||||
err := s.WriteFile(domain, keyExt, certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save key file: %w", err)
|
||||
}
|
||||
|
||||
if s.pem {
|
||||
err = s.WriteFile(domain, pemExt, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save PEM file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.pfx {
|
||||
err = s.WritePFXFile(domain, certRes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save PFX file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error {
|
||||
certPemBlock, _ := pem.Decode(certRes.Certificate)
|
||||
if certPemBlock == nil {
|
||||
return fmt.Errorf("unable to parse Certificate for domain %s", domain)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certPemBlock.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load Certificate for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
certChain, err := getCertificateChain(certRes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse PrivateKey for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
encoder, err := getPFXEncoder(s.pfxFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PFX encoder: %w", err)
|
||||
}
|
||||
|
||||
pfxBytes, err := encoder.Encode(privateKey, cert, certChain, s.pfxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
return s.WriteFile(domain, pfxExt, pfxBytes)
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) MoveToArchive(domain string) error {
|
||||
baseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))
|
||||
|
||||
matches, err := filepath.Glob(baseFilename + ".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, oldFile := range matches {
|
||||
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+issuerExt {
|
||||
continue
|
||||
}
|
||||
|
||||
date := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
filename := date + "." + filepath.Base(oldFile)
|
||||
newFile := filepath.Join(s.archivePath, filename)
|
||||
|
||||
err = os.Rename(oldFile, newFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {
|
||||
chainCertPemBlock, rest := pem.Decode(certRes.IssuerCertificate)
|
||||
if chainCertPemBlock == nil {
|
||||
return nil, errors.New("unable to parse Issuer Certificate")
|
||||
}
|
||||
|
||||
var certChain []*x509.Certificate
|
||||
|
||||
for chainCertPemBlock != nil {
|
||||
chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse Chain Certificate: %w", err)
|
||||
}
|
||||
|
||||
certChain = append(certChain, chainCert)
|
||||
chainCertPemBlock, rest = pem.Decode(rest) // Try decoding the next pem block
|
||||
}
|
||||
|
||||
return certChain, nil
|
||||
}
|
||||
|
||||
func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
|
||||
var encoder *pkcs12.Encoder
|
||||
|
||||
switch pfxFormat {
|
||||
case "SHA256":
|
||||
encoder = pkcs12.Modern2023
|
||||
case "DES":
|
||||
encoder = pkcs12.LegacyDES
|
||||
case "RC2":
|
||||
encoder = pkcs12.LegacyRC2
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid PFX format: %s", pfxFormat)
|
||||
}
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
|
||||
func sanitizedDomain(domain string) string {
|
||||
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
|
||||
if err != nil {
|
||||
log.Fatal("Could not sanitize the domain.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return safe
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,32 +6,53 @@ import (
|
|||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockCertificateStorage(t *testing.T) *CertificatesStorage {
|
||||
t.Helper()
|
||||
|
||||
basePath := t.TempDir()
|
||||
|
||||
writer, err := storage.NewCertificatesWriter(
|
||||
storage.CertificatesWriterConfig{
|
||||
BasePath: basePath,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
certStorage := &CertificatesStorage{
|
||||
CertificatesWriter: writer,
|
||||
CertificatesReader: storage.NewCertificatesReader(basePath),
|
||||
}
|
||||
|
||||
certStorage.CreateRootFolder()
|
||||
certStorage.CreateArchiveFolder()
|
||||
|
||||
return certStorage
|
||||
}
|
||||
|
||||
func TestCertificatesStorage_MoveToArchive(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
certStorage := mockCertificateStorage(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, domain)
|
||||
domainFiles := generateTestFiles(t, certStorage.GetRootPath(), domain)
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.NoFileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
root, err := os.ReadDir(certStorage.GetRootPath())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, root)
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
|
|
@ -41,25 +62,22 @@ func TestCertificatesStorage_MoveToArchive(t *testing.T) {
|
|||
func TestCertificatesStorage_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
certStorage := mockCertificateStorage(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, "example.org")
|
||||
domainFiles := generateTestFiles(t, certStorage.GetRootPath(), "example.org")
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
root, err := os.ReadDir(certStorage.GetRootPath())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, root, len(domainFiles))
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, archive)
|
||||
|
|
@ -68,15 +86,12 @@ func TestCertificatesStorage_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
|
|||
func TestCertificatesStorage_MoveToArchive_ambiguousDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
certStorage := mockCertificateStorage(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, domain)
|
||||
otherDomainFiles := generateTestFiles(t, storage.rootPath, domain+".example.org")
|
||||
domainFiles := generateTestFiles(t, certStorage.GetRootPath(), domain)
|
||||
otherDomainFiles := generateTestFiles(t, certStorage.GetRootPath(), domain+".example.org")
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
|
|
@ -87,11 +102,11 @@ func TestCertificatesStorage_MoveToArchive_ambiguousDomain(t *testing.T) {
|
|||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
root, err := os.ReadDir(certStorage.GetRootPath())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, root, len(otherDomainFiles))
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
|
|
@ -103,7 +118,7 @@ func generateTestFiles(t *testing.T, dir, domain string) []string {
|
|||
|
||||
var filenames []string
|
||||
|
||||
for _, ext := range []string{issuerExt, certExt, keyExt, pemExt, pfxExt, resourceExt} {
|
||||
for _, ext := range []string{storage.IssuerExt, storage.CertExt, storage.KeyExt, storage.PEMExt, storage.PFXExt, storage.ResourceExt} {
|
||||
filename := filepath.Join(dir, domain+ext)
|
||||
err := os.WriteFile(filename, []byte("test"), 0o666)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ func list(ctx context.Context, cmd *cli.Command) error {
|
|||
}
|
||||
|
||||
func listCertificates(_ context.Context, cmd *cli.Command) error {
|
||||
certsStorage := NewCertificatesStorage(cmd)
|
||||
certsStorage := storage.NewCertificatesReader(cmd.String(flgPath))
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
|
||||
if err != nil {
|
||||
|
|
@ -76,7 +77,7 @@ func listCertificates(_ context.Context, cmd *cli.Command) error {
|
|||
}
|
||||
|
||||
for _, filename := range matches {
|
||||
if strings.HasSuffix(filename, issuerExt) {
|
||||
if strings.HasSuffix(filename, storage.IssuerExt) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ func renew(ctx context.Context, cmd *cli.Command) error {
|
|||
log.Fatal("The account is not registered. Use 'run' to register a new account.", slog.String("email", account.Email))
|
||||
}
|
||||
|
||||
certsStorage := NewCertificatesStorage(cmd)
|
||||
certsStorage := newCertificatesStorage(cmd)
|
||||
|
||||
bundle := !cmd.Bool(flgNoBundle)
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, ke
|
|||
// load the cert resource from files.
|
||||
// We store the certificate, private key and metadata in different files
|
||||
// as web servers would not be able to work with a combined file.
|
||||
certificates, err := certsStorage.ReadCertificate(domain, certExt)
|
||||
certificates, err := certsStorage.ReadCertificate(domain, storage.CertExt)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, ke
|
|||
var privateKey crypto.PrivateKey
|
||||
|
||||
if cmd.Bool(flgReuseKey) {
|
||||
keyBytes, errR := certsStorage.ReadFile(domain, keyExt)
|
||||
keyBytes, errR := certsStorage.ReadFile(domain, storage.KeyExt)
|
||||
if errR != nil {
|
||||
log.Fatal("Error while loading the private key.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -322,7 +322,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyTyp
|
|||
// load the cert resource from files.
|
||||
// We store the certificate, private key and metadata in different files
|
||||
// as web servers would not be able to work with a combined file.
|
||||
certificates, err := certsStorage.ReadCertificate(domain, certExt)
|
||||
certificates, err := certsStorage.ReadCertificate(domain, storage.CertExt)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the certificate.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
|
|||
|
|
@ -57,13 +57,13 @@ func revoke(ctx context.Context, cmd *cli.Command) error {
|
|||
|
||||
client := newClient(cmd, account, keyType)
|
||||
|
||||
certsStorage := NewCertificatesStorage(cmd)
|
||||
certsStorage := newCertificatesStorage(cmd)
|
||||
certsStorage.CreateRootFolder()
|
||||
|
||||
for _, domain := range cmd.StringSlice(flgDomains) {
|
||||
log.Info("Trying to revoke the certificate.", log.DomainAttr(domain))
|
||||
|
||||
certBytes, err := certsStorage.ReadFile(domain, certExt)
|
||||
certBytes, err := certsStorage.ReadFile(domain, storage.CertExt)
|
||||
if err != nil {
|
||||
log.Fatal("Error while revoking the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ func run(ctx context.Context, cmd *cli.Command) error {
|
|||
fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
|
||||
}
|
||||
|
||||
certsStorage := NewCertificatesStorage(cmd)
|
||||
certsStorage := newCertificatesStorage(cmd)
|
||||
certsStorage.CreateRootFolder()
|
||||
|
||||
cert, err := obtainCertificate(ctx, cmd, client)
|
||||
|
|
@ -239,7 +239,7 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien
|
|||
if cmd.IsSet(flgPrivateKey) {
|
||||
var err error
|
||||
|
||||
request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey))
|
||||
request.PrivateKey, err = storage.LoadPrivateKey(cmd.String(flgPrivateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load private key: %w", err)
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien
|
|||
if cmd.IsSet(flgPrivateKey) {
|
||||
var err error
|
||||
|
||||
request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey))
|
||||
request.PrivateKey, err = storage.LoadPrivateKey(cmd.String(flgPrivateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load private key: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/urfave/cli/v3"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
|
|
@ -266,7 +267,7 @@ func CreatePathFlag(defaultPath string, forceCreation bool) cli.Flag {
|
|||
return nil
|
||||
}
|
||||
|
||||
err := createNonExistingFolder(s)
|
||||
err := storage.CreateNonExistingFolder(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not check/create the path %q: %w", s, err)
|
||||
}
|
||||
|
|
|
|||
15
cmd/hook.go
15
cmd/hook.go
|
|
@ -11,6 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -87,18 +88,18 @@ func metaToEnv(meta map[string]string) []string {
|
|||
|
||||
func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {
|
||||
meta[hookEnvCertDomain] = domain
|
||||
meta[hookEnvCertPath] = certsStorage.GetFileName(domain, certExt)
|
||||
meta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)
|
||||
meta[hookEnvCertPath] = certsStorage.GetFileName(domain, storage.CertExt)
|
||||
meta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, storage.KeyExt)
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
|
||||
meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, storage.IssuerExt)
|
||||
}
|
||||
|
||||
if certsStorage.pem {
|
||||
meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
|
||||
if certsStorage.IsPEM() {
|
||||
meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, storage.PEMExt)
|
||||
}
|
||||
|
||||
if certsStorage.pfx {
|
||||
meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
|
||||
if certsStorage.IsPFX() {
|
||||
meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, storage.PFXExt)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
cmd/internal/storage/certificates.go
Normal file
38
cmd/internal/storage/certificates.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
IssuerExt = ".issuer.crt"
|
||||
CertExt = ".crt"
|
||||
KeyExt = ".key"
|
||||
PEMExt = ".pem"
|
||||
PFXExt = ".pfx"
|
||||
ResourceExt = ".json"
|
||||
)
|
||||
|
||||
const (
|
||||
baseCertificatesFolderName = "certificates"
|
||||
baseArchivesFolderName = "archives"
|
||||
)
|
||||
|
||||
func getCertificatesRootPath(basePath string) string {
|
||||
return filepath.Join(basePath, baseCertificatesFolderName)
|
||||
}
|
||||
|
||||
func getCertificatesArchivePath(basePath string) string {
|
||||
return filepath.Join(basePath, baseArchivesFolderName)
|
||||
}
|
||||
|
||||
func CreateNonExistingFolder(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, 0o700)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
93
cmd/internal/storage/certificates_reader.go
Normal file
93
cmd/internal/storage/certificates_reader.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
type CertificatesReader struct {
|
||||
rootPath string
|
||||
}
|
||||
|
||||
func NewCertificatesReader(basePath string) *CertificatesReader {
|
||||
return &CertificatesReader{
|
||||
rootPath: getCertificatesRootPath(basePath),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) ReadResource(domain string) certificate.Resource {
|
||||
raw, err := s.ReadFile(domain, ResourceExt)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the metadata.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
var resource certificate.Resource
|
||||
if err = json.Unmarshal(raw, &resource); err != nil {
|
||||
log.Fatal("Error while marshaling the metadata.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) ExistsFile(domain, extension string) bool {
|
||||
filePath := s.GetFileName(domain, extension)
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Fatal("File stat", slog.String("filepath", filePath), log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) ReadFile(domain, extension string) ([]byte, error) {
|
||||
return os.ReadFile(s.GetFileName(domain, extension))
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) GetRootPath() string {
|
||||
return s.rootPath
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) GetFileName(domain, extension string) string {
|
||||
filename := sanitizedDomain(domain) + extension
|
||||
return filepath.Join(s.rootPath, filename)
|
||||
}
|
||||
|
||||
func (s *CertificatesReader) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
|
||||
content, err := s.ReadFile(domain, extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The input may be a bundle or a single certificate.
|
||||
return certcrypto.ParsePEMBundle(content)
|
||||
}
|
||||
|
||||
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
|
||||
func sanitizedDomain(domain string) string {
|
||||
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
|
||||
if err != nil {
|
||||
log.Fatal("Could not sanitize the domain.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return safe
|
||||
}
|
||||
301
cmd/internal/storage/certificates_writer.go
Normal file
301
cmd/internal/storage/certificates_writer.go
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
const filePerm os.FileMode = 0o600
|
||||
|
||||
type CertificatesWriterConfig struct {
|
||||
BasePath string
|
||||
|
||||
PEM bool
|
||||
PFX bool
|
||||
PFXFormat string
|
||||
PFXPassword string
|
||||
|
||||
Filename string // FIXME
|
||||
}
|
||||
|
||||
// CertificatesWriter a writer of certificate files.
|
||||
//
|
||||
// rootPath:
|
||||
//
|
||||
// ./.lego/certificates/
|
||||
// │ └── root certificates directory
|
||||
// └── "path" option
|
||||
//
|
||||
// archivePath:
|
||||
//
|
||||
// ./.lego/archives/
|
||||
// │ └── archived certificates directory
|
||||
// └── "path" option
|
||||
type CertificatesWriter struct {
|
||||
rootPath string
|
||||
archivePath string
|
||||
|
||||
pem bool
|
||||
|
||||
pfx bool
|
||||
pfxFormat string
|
||||
pfxPassword string
|
||||
|
||||
filename string // FIXME
|
||||
}
|
||||
|
||||
// NewCertificatesWriter create a new certificates storage writer.
|
||||
func NewCertificatesWriter(config CertificatesWriterConfig) (*CertificatesWriter, error) {
|
||||
if config.PFX {
|
||||
switch config.PFXFormat {
|
||||
case "DES", "RC2", "SHA256":
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid PFX format: %s", config.PFXFormat)
|
||||
}
|
||||
}
|
||||
|
||||
return &CertificatesWriter{
|
||||
rootPath: getCertificatesRootPath(config.BasePath),
|
||||
archivePath: getCertificatesArchivePath(config.BasePath),
|
||||
pem: config.PEM,
|
||||
pfx: config.PFX,
|
||||
pfxPassword: config.PFXPassword,
|
||||
pfxFormat: config.PFXFormat,
|
||||
filename: config.Filename,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) CreateRootFolder() {
|
||||
err := CreateNonExistingFolder(s.rootPath)
|
||||
if err != nil {
|
||||
log.Fatal("Could not check/create the root folder",
|
||||
slog.String("filepath", s.rootPath),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) CreateArchiveFolder() {
|
||||
err := CreateNonExistingFolder(s.archivePath)
|
||||
if err != nil {
|
||||
log.Fatal("Could not check/create the archive folder.",
|
||||
slog.String("filepath", s.archivePath),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) SaveResource(certRes *certificate.Resource) {
|
||||
domain := certRes.Domain
|
||||
|
||||
// We store the certificate, private key and metadata in different files
|
||||
// as web servers would not be able to work with a combined file.
|
||||
err := s.writeFile(domain, CertExt, certRes.Certificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save Certificate.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
err = s.writeFile(domain, IssuerExt, certRes.IssuerCertificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save IssuerCertificate.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// if we were given a CSR, we don't know the private key
|
||||
if certRes.PrivateKey != nil {
|
||||
err = s.writeCertificateFiles(domain, certRes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save PrivateKey.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
} else if s.pem || s.pfx {
|
||||
// we don't have the private key; can't write the .pem or .pfx file
|
||||
log.Fatal("Unable to save PEM or PFX without the private key. Are you using a CSR?", log.DomainAttr(domain))
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatal("Unable to marshal CertResource.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
err = s.writeFile(domain, ResourceExt, jsonBytes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save CertResource.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) MoveToArchive(domain string) error {
|
||||
baseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))
|
||||
|
||||
matches, err := filepath.Glob(baseFilename + ".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, oldFile := range matches {
|
||||
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+IssuerExt {
|
||||
continue
|
||||
}
|
||||
|
||||
date := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
filename := date + "." + filepath.Base(oldFile)
|
||||
newFile := filepath.Join(s.archivePath, filename)
|
||||
|
||||
err = os.Rename(oldFile, newFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) GetArchivePath() string {
|
||||
return s.archivePath
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) IsPEM() bool {
|
||||
return s.pem
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) IsPFX() bool {
|
||||
return s.pfx
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) writeCertificateFiles(domain string, certRes *certificate.Resource) error {
|
||||
err := s.writeFile(domain, KeyExt, certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save key file: %w", err)
|
||||
}
|
||||
|
||||
if s.pem {
|
||||
err = s.writeFile(domain, PEMExt, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save PEM file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.pfx {
|
||||
err = s.writePFXFile(domain, certRes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save PFX file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) writePFXFile(domain string, certRes *certificate.Resource) error {
|
||||
certPemBlock, _ := pem.Decode(certRes.Certificate)
|
||||
if certPemBlock == nil {
|
||||
return fmt.Errorf("unable to parse Certificate for domain %s", domain)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certPemBlock.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load Certificate for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
certChain, err := getCertificateChain(certRes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse PrivateKey for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
encoder, err := getPFXEncoder(s.pfxFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PFX encoder: %w", err)
|
||||
}
|
||||
|
||||
pfxBytes, err := encoder.Encode(privateKey, cert, certChain, s.pfxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
return s.writeFile(domain, PFXExt, pfxBytes)
|
||||
}
|
||||
|
||||
func (s *CertificatesWriter) writeFile(domain, extension string, data []byte) error {
|
||||
var baseFileName string
|
||||
if s.filename != "" {
|
||||
baseFileName = s.filename
|
||||
} else {
|
||||
baseFileName = sanitizedDomain(domain)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(s.rootPath, baseFileName+extension)
|
||||
|
||||
log.Info("Writing file.",
|
||||
slog.String("filepath", filePath))
|
||||
|
||||
return os.WriteFile(filePath, data, filePerm)
|
||||
}
|
||||
|
||||
func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {
|
||||
chainCertPemBlock, rest := pem.Decode(certRes.IssuerCertificate)
|
||||
if chainCertPemBlock == nil {
|
||||
return nil, errors.New("unable to parse Issuer Certificate")
|
||||
}
|
||||
|
||||
var certChain []*x509.Certificate
|
||||
|
||||
for chainCertPemBlock != nil {
|
||||
chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse Chain Certificate: %w", err)
|
||||
}
|
||||
|
||||
certChain = append(certChain, chainCert)
|
||||
chainCertPemBlock, rest = pem.Decode(rest) // Try decoding the next pem block
|
||||
}
|
||||
|
||||
return certChain, nil
|
||||
}
|
||||
|
||||
func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
|
||||
var encoder *pkcs12.Encoder
|
||||
|
||||
switch pfxFormat {
|
||||
case "SHA256":
|
||||
encoder = pkcs12.Modern2023
|
||||
case "DES":
|
||||
encoder = pkcs12.LegacyDES
|
||||
case "RC2":
|
||||
encoder = pkcs12.LegacyRC2
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid PFX format: %s", pfxFormat)
|
||||
}
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
12
cmd/setup.go
12
cmd/setup.go
|
|
@ -22,8 +22,6 @@ import (
|
|||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const filePerm os.FileMode = 0o600
|
||||
|
||||
// setupClient creates a new client with challenge settings.
|
||||
func setupClient(cmd *cli.Command, account *Account, keyType certcrypto.KeyType) *lego.Client {
|
||||
client := newClient(cmd, account, keyType)
|
||||
|
|
@ -123,16 +121,6 @@ func getUserAgent(cmd *cli.Command) string {
|
|||
return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", cmd.String(flgUserAgent), cmd.Version))
|
||||
}
|
||||
|
||||
func createNonExistingFolder(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, 0o700)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readCSRFile(filename string) (*x509.CertificateRequest, error) {
|
||||
bytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue