lego/cmd/internal/storage/certificates_writer.go
2026-03-11 17:15:45 +01:00

215 lines
5.6 KiB
Go

package storage
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log/slog"
"os"
"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
// SaveOptions contains the options for saving a certificate.
type SaveOptions struct {
PEM bool
PFX bool
PFXFormat string
PFXPassword string
}
// Validate validates the options.
func (o *SaveOptions) Validate() error {
if o == nil {
return nil
}
if o.PFX {
switch o.PFXFormat {
case "DES", "RC2", "SHA256":
default:
return fmt.Errorf("invalid PFX format: %s", o.PFXFormat)
}
}
return nil
}
// Save saves the certificate and related files.
// - the resource file (JSON)
// - the certificate file
// - the private key file (if any)
// - the issuer certificate file (if any)
// - the PFX file (if needed)
// - the PEM file (if needed).
func (s *CertificatesStorage) Save(certRes *certificate.Resource, opts *SaveOptions) error {
err := opts.Validate()
if err != nil {
return err
}
err = CreateNonExistingFolder(s.rootPath)
if err != nil {
return fmt.Errorf("root folder creation: %w", err)
}
err = s.writeFile(certRes.ID, ExtCert, certRes.Certificate)
if err != nil {
return fmt.Errorf("unable to save the certificate for %q: %w", certRes.ID, err)
}
if certRes.IssuerCertificate != nil {
err = s.writeFile(certRes.ID, ExtIssuer, certRes.IssuerCertificate)
if err != nil {
return fmt.Errorf("unable to save the issuer certificate for %q: %w", certRes.ID, err)
}
}
// if we were given a CSR, we don't know the private key
if certRes.PrivateKey != nil {
err = s.writeCertificateFiles(certRes, opts)
if err != nil {
return fmt.Errorf("unable to save the private key for %q: %w", certRes.ID, err)
}
} else if opts != nil && (opts.PEM || opts.PFX) {
// we don't have the private key; can't write the .pem or .pfx file
return fmt.Errorf("unable to save PEM or PFX without the private key for %q: probable usage of a CSR", certRes.ID)
}
return s.saveResource(certRes)
}
// Archive moves the certificate files to the archive folder.
func (s *CertificatesStorage) Archive(certID string) error {
return s.archiver.archiveCertificate(certID)
}
func (s *CertificatesStorage) saveResource(certRes *certificate.Resource) error {
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
return fmt.Errorf("unable to marshal the resource for %q: %w", certRes.ID, err)
}
err = s.writeFile(certRes.ID, ExtResource, jsonBytes)
if err != nil {
return fmt.Errorf("unable to save the resource for %q: %w", certRes.ID, err)
}
return nil
}
func (s *CertificatesStorage) writeCertificateFiles(certRes *certificate.Resource, opts *SaveOptions) error {
err := s.writeFile(certRes.ID, ExtKey, certRes.PrivateKey)
if err != nil {
return fmt.Errorf("unable to save the key file: %w", err)
}
if opts == nil {
return nil
}
if opts.PEM {
err = s.writeFile(certRes.ID, ExtPEM, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
if err != nil {
return fmt.Errorf("unable to save the PEM file: %w", err)
}
}
if opts.PFX {
err = s.writePFXFile(certRes, opts.PFXPassword, opts.PFXFormat)
if err != nil {
return fmt.Errorf("unable to save the PFX file: %w", err)
}
}
return nil
}
func (s *CertificatesStorage) writePFXFile(certRes *certificate.Resource, password, format string) error {
certPemBlock, _ := pem.Decode(certRes.Certificate)
if certPemBlock == nil {
return fmt.Errorf("unable to parse certificate %q", certRes.ID)
}
cert, err := x509.ParseCertificate(certPemBlock.Bytes)
if err != nil {
return fmt.Errorf("unable to load certificate %q: %w", certRes.ID, err)
}
certChain, err := getCertificateChain(certRes)
if err != nil {
return fmt.Errorf("unable to get certificate chain %q: %w", certRes.ID, err)
}
privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
if err != nil {
return fmt.Errorf("unable to parse private ky %q: %w", certRes.ID, err)
}
encoder, err := getPFXEncoder(format)
if err != nil {
return fmt.Errorf("PFX encoder: %w", err)
}
pfxBytes, err := encoder.Encode(privateKey, cert, certChain, password)
if err != nil {
return fmt.Errorf("unable to encode PFX data %q: %w", certRes.ID, err)
}
return s.writeFile(certRes.ID, ExtPFX, pfxBytes)
}
func (s *CertificatesStorage) writeFile(domain, extension string, data []byte) error {
filePath := s.GetFileName(domain, 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
}