mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
refactor(cli): storage and flags management (#2812)
This commit is contained in:
parent
12a393888c
commit
ab4e321904
35 changed files with 1741 additions and 808 deletions
|
|
@ -1,115 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCertificatesStorage_MoveToArchive(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, domain)
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.NoFileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, root)
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
|
||||
}
|
||||
|
||||
func TestCertificatesStorage_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, "example.org")
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, root, len(domainFiles))
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, archive)
|
||||
}
|
||||
|
||||
func TestCertificatesStorage_MoveToArchive_ambiguousDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
storage := CertificatesStorage{
|
||||
rootPath: t.TempDir(),
|
||||
archivePath: t.TempDir(),
|
||||
}
|
||||
|
||||
domainFiles := generateTestFiles(t, storage.rootPath, domain)
|
||||
otherDomainFiles := generateTestFiles(t, storage.rootPath, domain+".example.org")
|
||||
|
||||
err := storage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.NoFileExists(t, file)
|
||||
}
|
||||
|
||||
for _, file := range otherDomainFiles {
|
||||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(storage.rootPath)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, root, len(otherDomainFiles))
|
||||
|
||||
archive, err := os.ReadDir(storage.archivePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
|
||||
}
|
||||
|
||||
func generateTestFiles(t *testing.T, dir, domain string) []string {
|
||||
t.Helper()
|
||||
|
||||
var filenames []string
|
||||
|
||||
for _, ext := range []string{issuerExt, certExt, keyExt, pemExt, pfxExt, resourceExt} {
|
||||
filename := filepath.Join(dir, domain+ext)
|
||||
err := os.WriteFile(filename, []byte("test"), 0o666)
|
||||
require.NoError(t, err)
|
||||
|
||||
filenames = append(filenames, filename)
|
||||
}
|
||||
|
||||
return filenames
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Before(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
if cmd.String(flgPath) == "" {
|
||||
log.Fatal(fmt.Sprintf("Could not determine the current working directory. Please pass --%s.", flgPath))
|
||||
}
|
||||
|
||||
err := createNonExistingFolder(cmd.String(flgPath))
|
||||
if err != nil {
|
||||
log.Fatal("Could not check/create the path.",
|
||||
slog.String("flag", flgPath),
|
||||
slog.String("filepath", cmd.String(flgPath)),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
if cmd.String(flgServer) == "" {
|
||||
log.Fatal(fmt.Sprintf("Could not determine the current working server. Please pass --%s.", flgServer))
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
|
@ -17,12 +17,16 @@ func createDNSHelp() *cli.Command {
|
|||
Name: "dnshelp",
|
||||
Usage: "Shows additional help for the '--dns' global option",
|
||||
Action: dnsHelp,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flgCode,
|
||||
Aliases: []string{"c"},
|
||||
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
|
||||
},
|
||||
Flags: createDNSHelpFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func createDNSHelpFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flgCode,
|
||||
Aliases: []string{"c"},
|
||||
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -23,24 +24,23 @@ func createList() *cli.Command {
|
|||
Name: "list",
|
||||
Usage: "Display certificates and accounts information.",
|
||||
Action: list,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgAccounts,
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Display accounts.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNames,
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Display certificate common names only.",
|
||||
},
|
||||
// fake email, needed by NewAccountsStorage
|
||||
&cli.StringFlag{
|
||||
Name: flgEmail,
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
},
|
||||
Flags: createListFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func createListFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgAccounts,
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Display accounts.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNames,
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Display certificate names only.",
|
||||
},
|
||||
CreatePathFlag(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,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 {
|
||||
|
|
@ -77,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.ExtIssuer) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ func listCertificates(_ context.Context, cmd *cli.Command) error {
|
|||
}
|
||||
|
||||
func listAccount(_ context.Context, cmd *cli.Command) error {
|
||||
accountsStorage := NewAccountsStorage(cmd)
|
||||
accountsStorage := newAccountsStorage(cmd)
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
|
||||
if err != nil {
|
||||
|
|
@ -131,7 +131,7 @@ func listAccount(_ context.Context, cmd *cli.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var account Account
|
||||
var account storage.Account
|
||||
|
||||
err = json.Unmarshal(data, &account)
|
||||
if err != nil {
|
||||
|
|
|
|||
188
cmd/cmd_renew.go
188
cmd/cmd_renew.go
|
|
@ -17,6 +17,8 @@ import (
|
|||
"github.com/go-acme/lego/v5/acme/api"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/hook"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/mattn/go-isatty"
|
||||
|
|
@ -60,101 +62,109 @@ func createRenew() *cli.Command {
|
|||
|
||||
return ctx, nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: flgRenewDays,
|
||||
Value: 30,
|
||||
Usage: "The number of days left on a certificate to renew it.",
|
||||
},
|
||||
// TODO(ldez): in v5, remove this flag, use this behavior as default.
|
||||
&cli.BoolFlag{
|
||||
Name: flgRenewDynamic,
|
||||
Value: false,
|
||||
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgARIDisable,
|
||||
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgARIWaitToRenewDuration,
|
||||
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgReuseKey,
|
||||
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoBundle,
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgMustStaple,
|
||||
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
|
||||
" Only works if the CSR is generated by lego.",
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotBefore,
|
||||
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotAfter,
|
||||
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPreferredChain,
|
||||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgRenewHook,
|
||||
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRenewHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoRandomSleep,
|
||||
Usage: "Do not add a random sleep before the renewal." +
|
||||
" We do not recommend using this flag if you are doing your renewals in an automated way.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgForceCertDomains,
|
||||
Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.",
|
||||
},
|
||||
},
|
||||
Flags: createRenewFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func createRenewFlags() []cli.Flag {
|
||||
flags := CreateFlags()
|
||||
|
||||
flags = append(flags,
|
||||
&cli.IntFlag{
|
||||
Name: flgRenewDays,
|
||||
Value: 30,
|
||||
Usage: "The number of days left on a certificate to renew it.",
|
||||
},
|
||||
// TODO(ldez): in v5, remove this flag, use this behavior as default.
|
||||
&cli.BoolFlag{
|
||||
Name: flgRenewDynamic,
|
||||
Value: false,
|
||||
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgARIDisable,
|
||||
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgARIWaitToRenewDuration,
|
||||
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgReuseKey,
|
||||
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoBundle,
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgMustStaple,
|
||||
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
|
||||
" Only works if the CSR is generated by lego.",
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotBefore,
|
||||
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotAfter,
|
||||
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPreferredChain,
|
||||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgRenewHook,
|
||||
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRenewHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoRandomSleep,
|
||||
Usage: "Do not add a random sleep before the renewal." +
|
||||
" We do not recommend using this flag if you are doing your renewals in an automated way.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgForceCertDomains,
|
||||
Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.",
|
||||
},
|
||||
)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func renew(ctx context.Context, cmd *cli.Command) error {
|
||||
account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd))
|
||||
account, keyType := setupAccount(ctx, cmd, newAccountsStorage(cmd))
|
||||
|
||||
if account.Registration == nil {
|
||||
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)
|
||||
|
||||
meta := map[string]string{
|
||||
hookEnvAccountEmail: account.Email,
|
||||
hook.EnvAccountEmail: account.Email,
|
||||
}
|
||||
|
||||
// CSR
|
||||
|
|
@ -166,14 +176,14 @@ func renew(ctx context.Context, cmd *cli.Command) error {
|
|||
return renewForDomains(ctx, cmd, account, keyType, certsStorage, bundle, meta)
|
||||
}
|
||||
|
||||
func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
domains := cmd.StringSlice(flgDomains)
|
||||
domain := domains[0]
|
||||
|
||||
// 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.ExtCert)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
|
|
@ -234,7 +244,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.ExtKey)
|
||||
if errR != nil {
|
||||
log.Fatal("Error while loading the private key.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -293,10 +303,10 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, ke
|
|||
|
||||
addPathToMetadata(meta, domain, certRes, certsStorage)
|
||||
|
||||
return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
|
||||
return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
|
||||
}
|
||||
|
||||
func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
csr, err := readCSRFile(cmd.String(flgCSR))
|
||||
if err != nil {
|
||||
log.Fatal("Could not read CSR file.",
|
||||
|
|
@ -314,7 +324,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.ExtCert)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the certificate.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -392,7 +402,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyTyp
|
|||
|
||||
addPathToMetadata(meta, domain, certRes, certsStorage)
|
||||
|
||||
return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
|
||||
return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
|
||||
}
|
||||
|
||||
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
|
@ -20,28 +21,36 @@ func createRevoke() *cli.Command {
|
|||
Name: "revoke",
|
||||
Usage: "Revoke a certificate",
|
||||
Action: revoke,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgKeep,
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Keep the certificates after the revocation instead of archiving them.",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: flgReason,
|
||||
Usage: "Identifies the reason for the certificate revocation." +
|
||||
" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." +
|
||||
" Valid values are:" +
|
||||
" 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged)," +
|
||||
" 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL)," +
|
||||
" 9 (privilegeWithdrawn), or 10 (aACompromise).",
|
||||
Value: acme.CRLReasonUnspecified,
|
||||
},
|
||||
},
|
||||
Flags: createRevokeFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func createRevokeFlags() []cli.Flag {
|
||||
flags := CreateFlags()
|
||||
|
||||
flags = append(flags,
|
||||
&cli.BoolFlag{
|
||||
Name: flgKeep,
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Keep the certificates after the revocation instead of archiving them.",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: flgReason,
|
||||
Usage: "Identifies the reason for the certificate revocation." +
|
||||
" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." +
|
||||
" Valid values are:" +
|
||||
" 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged)," +
|
||||
" 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL)," +
|
||||
" 9 (privilegeWithdrawn), or 10 (aACompromise).",
|
||||
Value: acme.CRLReasonUnspecified,
|
||||
},
|
||||
)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func revoke(ctx context.Context, cmd *cli.Command) error {
|
||||
account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd))
|
||||
account, keyType := setupAccount(ctx, cmd, newAccountsStorage(cmd))
|
||||
|
||||
if account.Registration == nil {
|
||||
log.Fatal("Account is not registered. Use 'run' to register a new account.", slog.String("email", account.Email))
|
||||
|
|
@ -49,13 +58,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.ExtCert)
|
||||
if err != nil {
|
||||
log.Fatal("Error while revoking the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
|
|
|
|||
122
cmd/cmd_run.go
122
cmd/cmd_run.go
|
|
@ -10,6 +10,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/hook"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
|
|
@ -50,58 +52,66 @@ func createRun() *cli.Command {
|
|||
return ctx, nil
|
||||
},
|
||||
Action: run,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoBundle,
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgMustStaple,
|
||||
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
|
||||
" Only works if the CSR is generated by lego.",
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotBefore,
|
||||
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotAfter,
|
||||
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPrivateKey,
|
||||
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPreferredChain,
|
||||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgRunHook,
|
||||
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRunHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
Flags: createRunFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func createRunFlags() []cli.Flag {
|
||||
flags := CreateFlags()
|
||||
|
||||
flags = append(flags,
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoBundle,
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgMustStaple,
|
||||
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
|
||||
" Only works if the CSR is generated by lego.",
|
||||
},
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotBefore,
|
||||
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
}
|
||||
&cli.TimestampFlag{
|
||||
Name: flgNotAfter,
|
||||
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
|
||||
Config: cli.TimestampConfig{
|
||||
Layouts: []string{time.RFC3339},
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPrivateKey,
|
||||
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPreferredChain,
|
||||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgRunHook,
|
||||
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRunHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
},
|
||||
)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
const rootPathWarningMessage = `!!!! HEADS UP !!!!
|
||||
|
|
@ -116,7 +126,7 @@ server. Making regular backups of this folder is ideal.
|
|||
`
|
||||
|
||||
func run(ctx context.Context, cmd *cli.Command) error {
|
||||
accountsStorage := NewAccountsStorage(cmd)
|
||||
accountsStorage := newAccountsStorage(cmd)
|
||||
|
||||
account, keyType := setupAccount(ctx, cmd, accountsStorage)
|
||||
|
||||
|
|
@ -136,7 +146,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)
|
||||
|
|
@ -149,12 +159,12 @@ func run(ctx context.Context, cmd *cli.Command) error {
|
|||
certsStorage.SaveResource(cert)
|
||||
|
||||
meta := map[string]string{
|
||||
hookEnvAccountEmail: account.Email,
|
||||
hook.EnvAccountEmail: account.Email,
|
||||
}
|
||||
|
||||
addPathToMetadata(meta, cert.Domain, cert, certsStorage)
|
||||
|
||||
return launchHook(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta)
|
||||
return hook.Launch(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta)
|
||||
}
|
||||
|
||||
func handleTOS(cmd *cli.Command, client *lego.Client) bool {
|
||||
|
|
@ -231,7 +241,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)
|
||||
}
|
||||
|
|
@ -260,7 +270,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)
|
||||
}
|
||||
|
|
|
|||
304
cmd/flags.go
304
cmd/flags.go
|
|
@ -6,53 +6,75 @@ 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"
|
||||
)
|
||||
|
||||
// Flag names.
|
||||
// Flag names related to the account and domains.
|
||||
const (
|
||||
flgDomains = "domains"
|
||||
flgAcceptTOS = "accept-tos"
|
||||
flgEmail = "email"
|
||||
flgCSR = "csr"
|
||||
flgEAB = "eab"
|
||||
flgKID = "kid"
|
||||
flgHMAC = "hmac"
|
||||
)
|
||||
|
||||
// Flag names related to the output.
|
||||
const (
|
||||
flgFilename = "filename"
|
||||
flgPath = "path"
|
||||
flgPEM = "pem"
|
||||
flgPFX = "pfx"
|
||||
flgPFXPass = "pfx.pass"
|
||||
flgPFXFormat = "pfx.format"
|
||||
)
|
||||
|
||||
// Flag names related to the ACME client.
|
||||
const (
|
||||
flgServer = "server"
|
||||
flgDisableCommonName = "disable-cn"
|
||||
flgKeyType = "key-type"
|
||||
flgHTTPTimeout = "http-timeout"
|
||||
flgTLSSkipVerify = "tls-skip-verify"
|
||||
flgCertTimeout = "cert.timeout"
|
||||
flgOverallRequestLimit = "overall-request-limit"
|
||||
flgUserAgent = "user-agent"
|
||||
)
|
||||
|
||||
// Flag names related to HTTP-01 challenge.
|
||||
const (
|
||||
flgHTTP = "http"
|
||||
flgHTTPPort = "http.port"
|
||||
flgHTTPDelay = "http.delay"
|
||||
flgHTTPProxyHeader = "http.proxy-header"
|
||||
flgHTTPWebroot = "http.webroot"
|
||||
flgHTTPMemcachedHost = "http.memcached-host"
|
||||
flgHTTPS3Bucket = "http.s3-bucket"
|
||||
)
|
||||
|
||||
// Flag names related to TLS-ALPN-01 challenge.
|
||||
const (
|
||||
flgTLS = "tls"
|
||||
flgTLSPort = "tls.port"
|
||||
flgTLSDelay = "tls.delay"
|
||||
)
|
||||
|
||||
// Flag names related to DNS-01 challenge.
|
||||
const (
|
||||
flgDomains = "domains"
|
||||
flgServer = "server"
|
||||
flgAcceptTOS = "accept-tos"
|
||||
flgEmail = "email"
|
||||
flgDisableCommonName = "disable-cn"
|
||||
flgCSR = "csr"
|
||||
flgEAB = "eab"
|
||||
flgKID = "kid"
|
||||
flgHMAC = "hmac"
|
||||
flgKeyType = "key-type"
|
||||
flgFilename = "filename"
|
||||
flgPath = "path"
|
||||
flgHTTP = "http"
|
||||
flgHTTPPort = "http.port"
|
||||
flgHTTPDelay = "http.delay"
|
||||
flgHTTPProxyHeader = "http.proxy-header"
|
||||
flgHTTPWebroot = "http.webroot"
|
||||
flgHTTPMemcachedHost = "http.memcached-host"
|
||||
flgHTTPS3Bucket = "http.s3-bucket"
|
||||
flgTLS = "tls"
|
||||
flgTLSPort = "tls.port"
|
||||
flgTLSDelay = "tls.delay"
|
||||
flgDNS = "dns"
|
||||
flgDNSDisableCP = "dns.disable-cp"
|
||||
flgDNSPropagationWait = "dns.propagation-wait"
|
||||
flgDNSPropagationDisableANS = "dns.propagation-disable-ans"
|
||||
flgDNSPropagationRNS = "dns.propagation-rns"
|
||||
flgDNSResolvers = "dns.resolvers"
|
||||
flgHTTPTimeout = "http-timeout"
|
||||
flgTLSSkipVerify = "tls-skip-verify"
|
||||
flgDNSTimeout = "dns-timeout"
|
||||
flgPEM = "pem"
|
||||
flgPFX = "pfx"
|
||||
flgPFXPass = "pfx.pass"
|
||||
flgPFXFormat = "pfx.format"
|
||||
flgCertTimeout = "cert.timeout"
|
||||
flgOverallRequestLimit = "overall-request-limit"
|
||||
flgUserAgent = "user-agent"
|
||||
)
|
||||
|
||||
// Environment variable names.
|
||||
const (
|
||||
envEAB = "LEGO_EAB"
|
||||
envEABHMAC = "LEGO_EAB_HMAC"
|
||||
|
|
@ -65,78 +87,53 @@ const (
|
|||
envServer = "LEGO_SERVER"
|
||||
)
|
||||
|
||||
func CreateFlags(defaultPath string) []cli.Flag {
|
||||
if defaultPath == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
defaultPath = filepath.Join(cwd, ".lego")
|
||||
}
|
||||
}
|
||||
|
||||
func CreateACMEClientFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: flgDomains,
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Add a domain to the process. Can be specified multiple times.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgServer,
|
||||
Aliases: []string{"s"},
|
||||
Sources: cli.EnvVars(envServer),
|
||||
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
|
||||
Value: lego.LEDirectoryProduction,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgAcceptTOS,
|
||||
Aliases: []string{"a"},
|
||||
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgEmail,
|
||||
Aliases: []string{"m"},
|
||||
Sources: cli.EnvVars(envEmail),
|
||||
Usage: "Email used for registration and recovery contact.",
|
||||
Name: flgServer,
|
||||
Aliases: []string{"s"},
|
||||
Sources: cli.EnvVars(envServer),
|
||||
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
|
||||
Value: lego.LEDirectoryProduction,
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgDisableCommonName,
|
||||
Usage: "Disable the use of the common name in the CSR.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgCSR,
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Certificate signing request filename, if an external CSR is to be used.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgEAB,
|
||||
Sources: cli.EnvVars(envEAB),
|
||||
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgKID,
|
||||
Sources: cli.EnvVars(envEABKID),
|
||||
Usage: "Key identifier from External CA. Used for External Account Binding.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgHMAC,
|
||||
Sources: cli.EnvVars(envEABHMAC),
|
||||
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgKeyType,
|
||||
Aliases: []string{"k"},
|
||||
Value: "ec256",
|
||||
Usage: "Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgFilename,
|
||||
Usage: "(deprecated) Filename of the generated certificate.",
|
||||
&cli.IntFlag{
|
||||
Name: flgHTTPTimeout,
|
||||
Usage: "Set the HTTP timeout value to a specific value in seconds.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgTLSSkipVerify,
|
||||
Usage: "Skip the TLS verification of the ACME server.",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgCertTimeout,
|
||||
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
|
||||
Value: 30,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgOverallRequestLimit,
|
||||
Usage: "ACME overall requests limit.",
|
||||
Value: certificate.DefaultOverallRequestLimit,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPath,
|
||||
Sources: cli.EnvVars(envPath),
|
||||
Usage: "Directory to use for storing the data.",
|
||||
Value: defaultPath,
|
||||
Name: flgUserAgent,
|
||||
Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateHTTPChallengeFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgHTTP,
|
||||
Usage: "Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.",
|
||||
|
|
@ -169,6 +166,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
Name: flgHTTPS3Bucket,
|
||||
Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTLSChallengeFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgTLS,
|
||||
Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.",
|
||||
|
|
@ -183,6 +185,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
Usage: "Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.",
|
||||
Value: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDNSChallengeFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flgDNS,
|
||||
Usage: "Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
|
||||
|
|
@ -210,19 +217,21 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
" Supported: host:port." +
|
||||
" The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgHTTPTimeout,
|
||||
Usage: "Set the HTTP timeout value to a specific value in seconds.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgTLSSkipVerify,
|
||||
Usage: "Skip the TLS verification of the ACME server.",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgDNSTimeout,
|
||||
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries.",
|
||||
Value: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateOutputFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flgFilename,
|
||||
Usage: "(deprecated) Filename of the generated certificate.",
|
||||
},
|
||||
CreatePathFlag(true),
|
||||
&cli.BoolFlag{
|
||||
Name: flgPEM,
|
||||
Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.",
|
||||
|
|
@ -244,19 +253,102 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
Value: "RC2",
|
||||
Sources: cli.EnvVars(envPFXFormat),
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgCertTimeout,
|
||||
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
|
||||
Value: 30,
|
||||
}
|
||||
}
|
||||
|
||||
func CreatePathFlag(forceCreation bool) cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: flgPath,
|
||||
Sources: cli.NewValueSourceChain(cli.EnvVar(envPath), &defaultPathValueSource{}),
|
||||
Usage: "Directory to use for storing the data.",
|
||||
Validator: func(s string) error {
|
||||
if !forceCreation {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := storage.CreateNonExistingFolder(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not check/create the path %q: %w", s, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgOverallRequestLimit,
|
||||
Usage: "ACME overall requests limit.",
|
||||
Value: certificate.DefaultOverallRequestLimit,
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func CreateAccountFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: flgAcceptTOS,
|
||||
Aliases: []string{"a"},
|
||||
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgUserAgent,
|
||||
Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli",
|
||||
Name: flgEmail,
|
||||
Aliases: []string{"m"},
|
||||
Sources: cli.EnvVars(envEmail),
|
||||
Usage: "Email used for registration and recovery contact.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgCSR,
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Certificate signing request filename, if an external CSR is to be used.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgEAB,
|
||||
Sources: cli.EnvVars(envEAB),
|
||||
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgKID,
|
||||
Sources: cli.EnvVars(envEABKID),
|
||||
Usage: "Key identifier from External CA. Used for External Account Binding.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgHMAC,
|
||||
Sources: cli.EnvVars(envEABHMAC),
|
||||
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateFlags() []cli.Flag {
|
||||
flags := []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: flgDomains,
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Add a domain to the process. Can be specified multiple times.",
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, CreateAccountFlags()...)
|
||||
flags = append(flags, CreateACMEClientFlags()...)
|
||||
flags = append(flags, CreateOutputFlags()...)
|
||||
flags = append(flags, CreateHTTPChallengeFlags()...)
|
||||
flags = append(flags, CreateTLSChallengeFlags()...)
|
||||
flags = append(flags, CreateDNSChallengeFlags()...)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// defaultPathValueSource gets the default path based on the current working directory.
|
||||
// The field value is only here because clihelp/generator.
|
||||
type defaultPathValueSource struct{}
|
||||
|
||||
func (p *defaultPathValueSource) String() string {
|
||||
return "default path"
|
||||
}
|
||||
|
||||
func (p *defaultPathValueSource) GoString() string {
|
||||
return "&defaultPathValueSource{}"
|
||||
}
|
||||
|
||||
func (p *defaultPathValueSource) Lookup() (string, bool) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return filepath.Join(cwd, ".lego"), true
|
||||
}
|
||||
|
|
|
|||
99
cmd/hook.go
99
cmd/hook.go
|
|
@ -1,104 +1,25 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/hook"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
hookEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
|
||||
hookEnvCertDomain = "LEGO_CERT_DOMAIN"
|
||||
hookEnvCertPath = "LEGO_CERT_PATH"
|
||||
hookEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
|
||||
hookEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
|
||||
hookEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
|
||||
hookEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
|
||||
)
|
||||
|
||||
func launchHook(ctx context.Context, hook string, timeout time.Duration, meta map[string]string) error {
|
||||
if hook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxCmd, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
parts := strings.Fields(hook)
|
||||
|
||||
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
|
||||
|
||||
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start command: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctxCmd.Done()
|
||||
|
||||
if ctxCmd.Err() != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = stdout.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
|
||||
return errors.New("hook timed out")
|
||||
}
|
||||
|
||||
return fmt.Errorf("wait command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func metaToEnv(meta map[string]string) []string {
|
||||
var envs []string
|
||||
|
||||
for k, v := range meta {
|
||||
envs = append(envs, k+"="+v)
|
||||
}
|
||||
|
||||
return envs
|
||||
}
|
||||
|
||||
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[hook.EnvCertDomain] = domain
|
||||
meta[hook.EnvCertPath] = certsStorage.GetFileName(domain, storage.ExtCert)
|
||||
meta[hook.EnvCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtKey)
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
|
||||
meta[hook.EnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtIssuer)
|
||||
}
|
||||
|
||||
if certsStorage.pem {
|
||||
meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
|
||||
if certsStorage.IsPEM() {
|
||||
meta[hook.EnvCertPEMPath] = certsStorage.GetFileName(domain, storage.ExtPEM)
|
||||
}
|
||||
|
||||
if certsStorage.pfx {
|
||||
meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
|
||||
if certsStorage.IsPFX() {
|
||||
meta[hook.EnvCertPFXPath] = certsStorage.GetFileName(domain, storage.ExtPFX)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
cmd/internal/hook/hook.go
Normal file
84
cmd/internal/hook/hook.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
|
||||
EnvCertDomain = "LEGO_CERT_DOMAIN"
|
||||
EnvCertPath = "LEGO_CERT_PATH"
|
||||
EnvCertKeyPath = "LEGO_CERT_KEY_PATH"
|
||||
EnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
|
||||
EnvCertPEMPath = "LEGO_CERT_PEM_PATH"
|
||||
EnvCertPFXPath = "LEGO_CERT_PFX_PATH"
|
||||
)
|
||||
|
||||
func Launch(ctx context.Context, hook string, timeout time.Duration, meta map[string]string) error {
|
||||
if hook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxCmd, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
parts := strings.Fields(hook)
|
||||
|
||||
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
|
||||
|
||||
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start command: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctxCmd.Done()
|
||||
|
||||
if ctxCmd.Err() != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = stdout.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
|
||||
return errors.New("hook timed out")
|
||||
}
|
||||
|
||||
return fmt.Errorf("wait command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func metaToEnv(meta map[string]string) []string {
|
||||
var envs []string
|
||||
|
||||
for k, v := range meta {
|
||||
envs = append(envs, k+"="+v)
|
||||
}
|
||||
|
||||
return envs
|
||||
}
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
package cmd
|
||||
package hook
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_launchHook(t *testing.T) {
|
||||
err := launchHook(t.Context(), "echo foo", 1*time.Second, map[string]string{})
|
||||
func Test_Launch(t *testing.T) {
|
||||
err := Launch(t.Context(), "echo foo", 1*time.Second, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_launchHook_errors(t *testing.T) {
|
||||
func Test_Launch_errors(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
|
@ -54,8 +55,18 @@ func Test_launchHook_errors(t *testing.T) {
|
|||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := launchHook(t.Context(), test.hook, test.timeout, map[string]string{})
|
||||
err := Launch(t.Context(), test.hook, test.timeout, map[string]string{})
|
||||
require.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_metaToEnv(t *testing.T) {
|
||||
env := metaToEnv(map[string]string{
|
||||
"foo": "bar",
|
||||
})
|
||||
|
||||
expected := []string{"foo=bar"}
|
||||
|
||||
assert.Equal(t, expected, env)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
|
@ -13,6 +13,10 @@ type Account struct {
|
|||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func NewAccount(email string, key crypto.PrivateKey) *Account {
|
||||
return &Account{Email: email, key: key}
|
||||
}
|
||||
|
||||
/** Implementation of the registration.User interface **/
|
||||
|
||||
// GetEmail returns the email address for the account.
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
package cmd
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -15,7 +16,6 @@ import (
|
|||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const userIDPlaceholder = "noemail@example.com"
|
||||
|
|
@ -26,6 +26,14 @@ const (
|
|||
accountFileName = "account.json"
|
||||
)
|
||||
|
||||
type AccountsStorageConfig struct {
|
||||
Email string
|
||||
BasePath string
|
||||
|
||||
Server string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// AccountsStorage A storage for account data.
|
||||
//
|
||||
// rootPath:
|
||||
|
|
@ -63,32 +71,28 @@ type AccountsStorage struct {
|
|||
userID string
|
||||
email string
|
||||
rootPath string
|
||||
rootUserPath string
|
||||
keysPath string
|
||||
accountFilePath string
|
||||
cmd *cli.Command
|
||||
|
||||
server *url.URL
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// NewAccountsStorage Creates a new AccountsStorage.
|
||||
func NewAccountsStorage(cmd *cli.Command) *AccountsStorage {
|
||||
// TODO: move to account struct?
|
||||
email := cmd.String(flgEmail)
|
||||
func NewAccountsStorage(config AccountsStorageConfig) (*AccountsStorage, error) {
|
||||
email := config.Email
|
||||
|
||||
userID := email
|
||||
if userID == "" {
|
||||
userID = userIDPlaceholder
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(cmd.String(flgServer))
|
||||
serverURL, err := url.Parse(config.Server)
|
||||
if err != nil {
|
||||
log.Fatal("URL parsing",
|
||||
slog.String("flag", flgServer),
|
||||
slog.String("serverURL", cmd.String(flgServer)),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
return nil, fmt.Errorf("invalid server URL %q: %w", config.Server, err)
|
||||
}
|
||||
|
||||
rootPath := filepath.Join(cmd.String(flgPath), baseAccountsRootFolderName)
|
||||
rootPath := filepath.Join(config.BasePath, baseAccountsRootFolderName)
|
||||
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
|
||||
accountsPath := filepath.Join(rootPath, serverPath)
|
||||
rootUserPath := filepath.Join(accountsPath, userID)
|
||||
|
|
@ -97,20 +101,19 @@ func NewAccountsStorage(cmd *cli.Command) *AccountsStorage {
|
|||
userID: userID,
|
||||
email: email,
|
||||
rootPath: rootPath,
|
||||
rootUserPath: rootUserPath,
|
||||
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
|
||||
accountFilePath: filepath.Join(rootUserPath, accountFileName),
|
||||
cmd: cmd,
|
||||
}
|
||||
server: serverURL,
|
||||
userAgent: config.UserAgent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) ExistsAccountFilePath() bool {
|
||||
accountFile := filepath.Join(s.rootUserPath, accountFileName)
|
||||
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(s.accountFilePath); os.IsNotExist(err) {
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Fatal("Could not read the account file.",
|
||||
slog.String("filepath", accountFile),
|
||||
slog.String("filepath", s.accountFilePath),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
|
@ -122,10 +125,6 @@ func (s *AccountsStorage) GetRootPath() string {
|
|||
return s.rootPath
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) GetRootUserPath() string {
|
||||
return s.rootUserPath
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) GetUserID() string {
|
||||
return s.userID
|
||||
}
|
||||
|
|
@ -165,7 +164,7 @@ func (s *AccountsStorage) LoadAccount(ctx context.Context, privateKey crypto.Pri
|
|||
account.key = privateKey
|
||||
|
||||
if account.Registration == nil || account.Registration.Body.Status == "" {
|
||||
reg, err := tryRecoverRegistration(ctx, s.cmd, privateKey)
|
||||
reg, err := s.tryRecoverRegistration(ctx, privateKey)
|
||||
if err != nil {
|
||||
log.Fatal("Could not load the account file. Registration is nil.",
|
||||
slog.String("userID", s.GetUserID()),
|
||||
|
|
@ -210,7 +209,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva
|
|||
return privateKey
|
||||
}
|
||||
|
||||
privateKey, err := loadPrivateKey(accKeyPath)
|
||||
privateKey, err := LoadPrivateKey(accKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal("Could not load an RSA private key from the file.",
|
||||
slog.String("filepath", accKeyPath),
|
||||
|
|
@ -222,7 +221,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva
|
|||
}
|
||||
|
||||
func (s *AccountsStorage) createKeysFolder() {
|
||||
if err := createNonExistingFolder(s.keysPath); err != nil {
|
||||
if err := CreateNonExistingFolder(s.keysPath); err != nil {
|
||||
log.Fatal("Could not check/create the directory for the account.",
|
||||
slog.String("userID", s.GetUserID()),
|
||||
log.ErrorAttr(err),
|
||||
|
|
@ -230,6 +229,39 @@ func (s *AccountsStorage) createKeysFolder() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) tryRecoverRegistration(ctx context.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
|
||||
// couldn't load account but got a key. Try to look the account up.
|
||||
config := lego.NewConfig(&Account{key: privateKey})
|
||||
config.CADirURL = s.server.String()
|
||||
config.UserAgent = s.userAgent
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg, err := client.Registration.ResolveAccountByKey(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
func LoadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
|
||||
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
|
||||
if err != nil {
|
||||
|
|
@ -251,36 +283,3 @@ func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.Private
|
|||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func tryRecoverRegistration(ctx context.Context, cmd *cli.Command, privateKey crypto.PrivateKey) (*registration.Resource, error) {
|
||||
// couldn't load account but got a key. Try to look the account up.
|
||||
config := lego.NewConfig(&Account{key: privateKey})
|
||||
config.CADirURL = cmd.String(flgServer)
|
||||
config.UserAgent = getUserAgent(cmd)
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg, err := client.Registration.ResolveAccountByKey(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
223
cmd/internal/storage/accounts_storage_test.go
Normal file
223
cmd/internal/storage/accounts_storage_test.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccountsStorage_GetUserID(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
email string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "with email",
|
||||
email: "test@example.com",
|
||||
expected: "test@example.com",
|
||||
},
|
||||
{
|
||||
desc: "without email",
|
||||
expected: "noemail@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
Email: test.email,
|
||||
BasePath: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.email, storage.GetEmail())
|
||||
assert.Equal(t, test.expected, storage.GetUserID())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountsStorage_ExistsAccountFilePath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
setup func(t *testing.T, storage *AccountsStorage)
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "an account file exists",
|
||||
setup: func(t *testing.T, storage *AccountsStorage) {
|
||||
t.Helper()
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(storage.accountFilePath), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(storage.accountFilePath, []byte("test"), 0o644)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "no account file",
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
BasePath: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if test.setup != nil {
|
||||
test.setup(t, storage)
|
||||
}
|
||||
|
||||
test.assert(t, storage.ExistsAccountFilePath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountsStorage_GetRootPath(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
BasePath: basePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Truef(t, strings.HasPrefix(storage.GetRootPath(), basePath),
|
||||
"%s is not prefixed with %s", storage.GetRootPath(), basePath)
|
||||
|
||||
rootPath, err := filepath.Rel(basePath, storage.GetRootPath())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, baseAccountsRootFolderName, rootPath)
|
||||
}
|
||||
|
||||
func TestAccountsStorage_Save(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
Email: "test@example.com",
|
||||
BasePath: basePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &Account{
|
||||
Email: "account@example.com",
|
||||
Registration: ®istration.Resource{
|
||||
Body: acme.Account{
|
||||
Status: "valid",
|
||||
Contact: []string{"contact@example.com"},
|
||||
TermsOfServiceAgreed: true,
|
||||
Orders: "https://ame.example.com/orders/123456",
|
||||
OnlyReturnExisting: true,
|
||||
ExternalAccountBinding: []byte(`"EAB"`),
|
||||
},
|
||||
URI: "https://ame.example.com",
|
||||
},
|
||||
key: crypto.PrivateKey(""),
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(storage.accountFilePath), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = storage.Save(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.FileExists(t, storage.accountFilePath)
|
||||
|
||||
accountFilePath, err := filepath.Rel(basePath, storage.accountFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, filepath.Join(baseAccountsRootFolderName, "test@example.com", accountFileName), accountFilePath)
|
||||
|
||||
file, err := os.ReadFile(storage.accountFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", accountFileName))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expected), string(file))
|
||||
}
|
||||
|
||||
func TestAccountsStorage_LoadAccount(t *testing.T) {
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
Email: "test@example.com",
|
||||
BasePath: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
storage.accountFilePath = filepath.Join("testdata", accountFileName)
|
||||
|
||||
account := storage.LoadAccount(t.Context(), "")
|
||||
|
||||
expected := &Account{
|
||||
Email: "account@example.com",
|
||||
Registration: ®istration.Resource{
|
||||
Body: acme.Account{
|
||||
Status: "valid",
|
||||
Contact: []string{"contact@example.com"},
|
||||
TermsOfServiceAgreed: true,
|
||||
Orders: "https://ame.example.com/orders/123456",
|
||||
OnlyReturnExisting: true,
|
||||
ExternalAccountBinding: []byte(`"EAB"`),
|
||||
},
|
||||
URI: "https://ame.example.com",
|
||||
},
|
||||
key: crypto.PrivateKey(""),
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, account)
|
||||
}
|
||||
|
||||
func TestAccountsStorage_GetPrivateKey(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
basePath string
|
||||
}{
|
||||
{
|
||||
desc: "create a new private key",
|
||||
},
|
||||
{
|
||||
desc: "existing private key",
|
||||
basePath: "testdata",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if test.basePath == "" {
|
||||
test.basePath = t.TempDir()
|
||||
}
|
||||
|
||||
storage, err := NewAccountsStorage(AccountsStorageConfig{
|
||||
Email: "test@example.com",
|
||||
BasePath: test.basePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(test.basePath, baseAccountsRootFolderName, "test@example.com", baseKeysFolderName, "test@example.com.key")
|
||||
|
||||
privateKey := storage.GetPrivateKey(certcrypto.RSA4096)
|
||||
|
||||
assert.FileExists(t, expectedPath)
|
||||
|
||||
assert.IsType(t, &rsa.PrivateKey{}, privateKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
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 (
|
||||
ExtIssuer = ".issuer.crt"
|
||||
ExtCert = ".crt"
|
||||
ExtKey = ".key"
|
||||
ExtPEM = ".pem"
|
||||
ExtPFX = ".pfx"
|
||||
ExtResource = ".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, ExtResource)
|
||||
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
|
||||
}
|
||||
141
cmd/internal/storage/certificates_reader_test.go
Normal file
141
cmd/internal/storage/certificates_reader_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCertificatesWriter_ReadResource(t *testing.T) {
|
||||
reader := NewCertificatesReader("testdata")
|
||||
|
||||
resource := reader.ReadResource("example.com")
|
||||
|
||||
expected := certificate.Resource{
|
||||
Domain: "example.com",
|
||||
CertURL: "https://acme.example.org/cert/123",
|
||||
CertStableURL: "https://acme.example.org/cert/456",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, resource)
|
||||
}
|
||||
|
||||
func TestNewCertificatesWriter_ExistsFile(t *testing.T) {
|
||||
reader := NewCertificatesReader("testdata")
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
domain string
|
||||
extension string
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "exists",
|
||||
domain: "example.com",
|
||||
extension: ExtResource,
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "not exists",
|
||||
domain: "example.org",
|
||||
extension: ExtResource,
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
test.assert(t, reader.ExistsFile(test.domain, test.extension))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCertificatesWriter_ReadFile(t *testing.T) {
|
||||
reader := NewCertificatesReader("testdata")
|
||||
|
||||
data, err := reader.ReadFile("example.com", ExtResource)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestNewCertificatesWriter_GetRootPath(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
|
||||
reader := NewCertificatesReader(basePath)
|
||||
|
||||
rootPath := reader.GetRootPath()
|
||||
|
||||
expected := filepath.Join(basePath, baseCertificatesFolderName)
|
||||
|
||||
assert.Equal(t, expected, rootPath)
|
||||
}
|
||||
|
||||
func TestNewCertificatesWriter_GetFileName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
domain string
|
||||
extension string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
domain: "example.com",
|
||||
extension: ExtCert,
|
||||
expected: "example.com.crt",
|
||||
},
|
||||
{
|
||||
desc: "hyphen",
|
||||
domain: "test-acme.example.com",
|
||||
extension: ExtResource,
|
||||
expected: "test-acme.example.com.json",
|
||||
},
|
||||
{
|
||||
desc: "wildcard",
|
||||
domain: "*.example.com",
|
||||
extension: ExtKey,
|
||||
expected: "_.example.com.key",
|
||||
},
|
||||
{
|
||||
desc: "colon",
|
||||
domain: "acme:test.example.com",
|
||||
extension: ExtResource,
|
||||
expected: "acme-test.example.com.json",
|
||||
},
|
||||
{
|
||||
desc: "IDN",
|
||||
domain: "测试.com",
|
||||
extension: ExtResource,
|
||||
expected: "xn--0zwm56d.com.json",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
basePath := t.TempDir()
|
||||
|
||||
reader := NewCertificatesReader(basePath)
|
||||
|
||||
filename := reader.GetFileName(test.domain, test.extension)
|
||||
|
||||
expected := filepath.Join(basePath, baseCertificatesFolderName, test.expected)
|
||||
|
||||
assert.Equal(t, expected, filename)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCertificatesWriter_ReadCertificate(t *testing.T) {
|
||||
reader := NewCertificatesReader("testdata")
|
||||
|
||||
cert, err := reader.ReadCertificate("example.org", ExtCert)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, cert)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -17,26 +17,23 @@ import (
|
|||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"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 filePerm os.FileMode = 0o600
|
||||
|
||||
const (
|
||||
issuerExt = ".issuer.crt"
|
||||
certExt = ".crt"
|
||||
keyExt = ".key"
|
||||
pemExt = ".pem"
|
||||
pfxExt = ".pfx"
|
||||
resourceExt = ".json"
|
||||
)
|
||||
type CertificatesWriterConfig struct {
|
||||
BasePath string
|
||||
|
||||
// CertificatesStorage a certificates' storage.
|
||||
PEM bool
|
||||
PFX bool
|
||||
PFXFormat string
|
||||
PFXPassword string
|
||||
|
||||
Filename string // TODO(ldez): remove
|
||||
}
|
||||
|
||||
// CertificatesWriter a writer of certificate files.
|
||||
//
|
||||
// rootPath:
|
||||
//
|
||||
|
|
@ -49,39 +46,42 @@ const (
|
|||
// ./.lego/archives/
|
||||
// │ └── archived certificates directory
|
||||
// └── "path" option
|
||||
type CertificatesStorage struct {
|
||||
type CertificatesWriter struct {
|
||||
rootPath string
|
||||
archivePath string
|
||||
pem bool
|
||||
|
||||
pem bool
|
||||
|
||||
pfx bool
|
||||
pfxPassword string
|
||||
pfxFormat string
|
||||
filename string // Deprecated
|
||||
pfxPassword string
|
||||
|
||||
filename string // TODO(ldez): remove
|
||||
}
|
||||
|
||||
// NewCertificatesStorage create a new certificates storage.
|
||||
func NewCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
|
||||
pfxFormat := cmd.String(flgPFXFormat)
|
||||
|
||||
switch pfxFormat {
|
||||
case "DES", "RC2", "SHA256":
|
||||
default:
|
||||
log.Fatal("Invalid PFX format.", slog.String("format", pfxFormat))
|
||||
// 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 &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),
|
||||
}
|
||||
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 *CertificatesStorage) CreateRootFolder() {
|
||||
err := createNonExistingFolder(s.rootPath)
|
||||
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),
|
||||
|
|
@ -90,8 +90,8 @@ func (s *CertificatesStorage) CreateRootFolder() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) CreateArchiveFolder() {
|
||||
err := createNonExistingFolder(s.archivePath)
|
||||
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),
|
||||
|
|
@ -100,16 +100,12 @@ func (s *CertificatesStorage) CreateArchiveFolder() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) GetRootPath() string {
|
||||
return s.rootPath
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
||||
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)
|
||||
err := s.writeFile(domain, ExtCert, certRes.Certificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save Certificate.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -118,7 +114,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
|||
}
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
err = s.WriteFile(domain, issuerExt, certRes.IssuerCertificate)
|
||||
err = s.writeFile(domain, ExtIssuer, certRes.IssuerCertificate)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save IssuerCertificate.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -129,7 +125,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
|||
|
||||
// if we were given a CSR, we don't know the private key
|
||||
if certRes.PrivateKey != nil {
|
||||
err = s.WriteCertificateFiles(domain, certRes)
|
||||
err = s.writeCertificateFiles(domain, certRes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save PrivateKey.", log.DomainAttr(domain), log.ErrorAttr(err))
|
||||
}
|
||||
|
|
@ -146,7 +142,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
|||
)
|
||||
}
|
||||
|
||||
err = s.WriteFile(domain, resourceExt, jsonBytes)
|
||||
err = s.writeFile(domain, ExtResource, jsonBytes)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to save CertResource.",
|
||||
log.DomainAttr(domain),
|
||||
|
|
@ -155,85 +151,59 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
|
||||
raw, err := s.ReadFile(domain, resourceExt)
|
||||
func (s *CertificatesWriter) MoveToArchive(domain string) error {
|
||||
baseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))
|
||||
|
||||
matches, err := filepath.Glob(baseFilename + ".*")
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading the metadata.",
|
||||
log.DomainAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
return 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),
|
||||
)
|
||||
for _, oldFile := range matches {
|
||||
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+ExtIssuer {
|
||||
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 resource
|
||||
return nil
|
||||
}
|
||||
|
||||
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 *CertificatesWriter) GetArchivePath() string {
|
||||
return s.archivePath
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
|
||||
return os.ReadFile(s.GetFileName(domain, extension))
|
||||
func (s *CertificatesWriter) IsPEM() bool {
|
||||
return s.pem
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
|
||||
filename := sanitizedDomain(domain) + extension
|
||||
return filepath.Join(s.rootPath, filename)
|
||||
func (s *CertificatesWriter) IsPFX() bool {
|
||||
return s.pfx
|
||||
}
|
||||
|
||||
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)
|
||||
func (s *CertificatesWriter) writeCertificateFiles(domain string, certRes *certificate.Resource) error {
|
||||
err := s.writeFile(domain, ExtKey, 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))
|
||||
err = s.writeFile(domain, ExtPEM, 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)
|
||||
err = s.writePFXFile(domain, certRes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save PFX file: %w", err)
|
||||
}
|
||||
|
|
@ -242,7 +212,7 @@ func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *cert
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error {
|
||||
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)
|
||||
|
|
@ -273,33 +243,23 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
|
|||
return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
return s.WriteFile(domain, pfxExt, pfxBytes)
|
||||
return s.writeFile(domain, ExtPFX, 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
|
||||
func (s *CertificatesWriter) writeFile(domain, extension string, data []byte) error {
|
||||
var baseFileName string
|
||||
if s.filename != "" {
|
||||
baseFileName = s.filename
|
||||
} else {
|
||||
baseFileName = sanitizedDomain(domain)
|
||||
}
|
||||
|
||||
for _, oldFile := range matches {
|
||||
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+issuerExt {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(s.rootPath, baseFileName+extension)
|
||||
|
||||
date := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
filename := date + "." + filepath.Base(oldFile)
|
||||
newFile := filepath.Join(s.archivePath, filename)
|
||||
log.Info("Writing file.",
|
||||
slog.String("filepath", filePath))
|
||||
|
||||
err = os.Rename(oldFile, newFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.WriteFile(filePath, data, filePerm)
|
||||
}
|
||||
|
||||
func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {
|
||||
|
|
@ -339,16 +299,3 @@ func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
|
|||
|
||||
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
|
||||
}
|
||||
285
cmd/internal/storage/certificates_writer_test.go
Normal file
285
cmd/internal/storage/certificates_writer_test.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCertificatesWriter_CreateRootFolder(t *testing.T) {
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoDirExists(t, writer.rootPath)
|
||||
|
||||
writer.CreateRootFolder()
|
||||
|
||||
require.DirExists(t, writer.rootPath)
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_CreateArchiveFolder(t *testing.T) {
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: t.TempDir(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoDirExists(t, writer.GetArchivePath())
|
||||
|
||||
writer.CreateArchiveFolder()
|
||||
|
||||
require.DirExists(t, writer.GetArchivePath())
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_SaveResource(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: basePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(writer.rootPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.crt"))
|
||||
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.issuer"))
|
||||
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.key"))
|
||||
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
|
||||
|
||||
writer.SaveResource(&certificate.Resource{
|
||||
Domain: "example.com",
|
||||
CertURL: "https://acme.example.org/cert/123",
|
||||
CertStableURL: "https://acme.example.org/cert/456",
|
||||
PrivateKey: []byte("PrivateKey"),
|
||||
Certificate: []byte("Certificate"),
|
||||
IssuerCertificate: []byte("IssuerCertificate"),
|
||||
CSR: []byte("CSR"),
|
||||
})
|
||||
|
||||
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.crt"))
|
||||
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.issuer.crt"))
|
||||
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.key"))
|
||||
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
|
||||
|
||||
assertCertificateFileContent(t, basePath, "example.com.crt")
|
||||
assertCertificateFileContent(t, basePath, "example.com.issuer.crt")
|
||||
assertCertificateFileContent(t, basePath, "example.com.key")
|
||||
|
||||
actual, err := os.ReadFile(filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", baseCertificatesFolderName, "example.com.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(expected), string(actual))
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_MoveToArchive(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
certStorage := setupCertificatesWriter(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, certStorage.rootPath, domain)
|
||||
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.NoFileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(certStorage.rootPath)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, root)
|
||||
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
certStorage := setupCertificatesWriter(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, certStorage.rootPath, "example.org")
|
||||
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(certStorage.rootPath)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, root, len(domainFiles))
|
||||
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, archive)
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_MoveToArchive_ambiguousDomain(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
certStorage := setupCertificatesWriter(t)
|
||||
|
||||
domainFiles := generateTestFiles(t, certStorage.rootPath, domain)
|
||||
otherDomainFiles := generateTestFiles(t, certStorage.rootPath, domain+".example.org")
|
||||
|
||||
err := certStorage.MoveToArchive(domain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, file := range domainFiles {
|
||||
assert.NoFileExists(t, file)
|
||||
}
|
||||
|
||||
for _, file := range otherDomainFiles {
|
||||
assert.FileExists(t, file)
|
||||
}
|
||||
|
||||
root, err := os.ReadDir(certStorage.rootPath)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, root, len(otherDomainFiles))
|
||||
|
||||
archive, err := os.ReadDir(certStorage.GetArchivePath())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, archive, len(domainFiles))
|
||||
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_GetArchivePath(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: basePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, filepath.Join(basePath, baseArchivesFolderName), writer.GetArchivePath())
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_IsPEM(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
pem bool
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "PEM enable",
|
||||
pem: true,
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "PEM disable",
|
||||
pem: false,
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: t.TempDir(),
|
||||
PEM: test.pem,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
test.assert(t, writer.IsPEM())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificatesWriter_IsPFX(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
pfx bool
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "PFX enable",
|
||||
pfx: true,
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "PFX disable",
|
||||
pfx: false,
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
|
||||
BasePath: t.TempDir(),
|
||||
PFX: test.pfx,
|
||||
PFXFormat: "DES",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
test.assert(t, writer.IsPFX())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCertificateFileContent(t *testing.T, basePath, filename string) {
|
||||
t.Helper()
|
||||
|
||||
actual, err := os.ReadFile(filepath.Join(basePath, baseCertificatesFolderName, filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", baseCertificatesFolderName, filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(expected), string(actual))
|
||||
}
|
||||
|
||||
func setupCertificatesWriter(t *testing.T) *CertificatesWriter {
|
||||
t.Helper()
|
||||
|
||||
basePath := t.TempDir()
|
||||
|
||||
writer, err := NewCertificatesWriter(
|
||||
CertificatesWriterConfig{
|
||||
BasePath: basePath,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
writer.CreateRootFolder()
|
||||
writer.CreateArchiveFolder()
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
func generateTestFiles(t *testing.T, dir, domain string) []string {
|
||||
t.Helper()
|
||||
|
||||
var filenames []string
|
||||
|
||||
for _, ext := range []string{ExtIssuer, ExtCert, ExtKey, ExtPEM, ExtPFX, ExtResource} {
|
||||
filename := filepath.Join(dir, domain+ext)
|
||||
err := os.WriteFile(filename, []byte("test"), 0o666)
|
||||
require.NoError(t, err)
|
||||
|
||||
filenames = append(filenames, filename)
|
||||
}
|
||||
|
||||
return filenames
|
||||
}
|
||||
16
cmd/internal/storage/testdata/account.json
vendored
Normal file
16
cmd/internal/storage/testdata/account.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"email": "account@example.com",
|
||||
"registration": {
|
||||
"body": {
|
||||
"status": "valid",
|
||||
"contact": [
|
||||
"contact@example.com"
|
||||
],
|
||||
"termsOfServiceAgreed": true,
|
||||
"orders": "https://ame.example.com/orders/123456",
|
||||
"onlyReturnExisting": true,
|
||||
"externalAccountBinding": "EAB"
|
||||
},
|
||||
"uri": "https://ame.example.com"
|
||||
}
|
||||
}
|
||||
51
cmd/internal/storage/testdata/accounts/test@example.com/keys/test@example.com.key
vendored
Normal file
51
cmd/internal/storage/testdata/accounts/test@example.com/keys/test@example.com.key
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJJwIBAAKCAgEAxF6apvKKtm33tZbeYwsS2v3BAR6Q1UM4PzyTnckyjlqQ+9p6
|
||||
IX3ZTDfatAw4D7Xzw3LQ8YwsNBJPRmx3SFtcbpWsuRtFFtnNuPlxw4UcM2QhWaq1
|
||||
55R6TMn8dcD+TgQWX5yG3dJBAAHNwtsxbGHjlafu6DHqFrhE63O7Pa0yV/Ld7lyg
|
||||
CrbIfEEroKPZxXco5VyxH+HceAhBqYe2j7ACWy5G3VlFS2zM3tor5u15bO7XGJAv
|
||||
Q1jiIG05TWxm0cfSq6IiJ3vaWlvwEmHFBxSmB/6/IGblw0FKLf2lUOYma87xJeS4
|
||||
2TjPYw+cZRqSYJeUBpXGuoGZq600PvpNwoPlT9umxFfAIOSLcKFzEqMG73sJVgO6
|
||||
TgDl+YYdkWQ0iCBmanUkkUUw11ESO3jIk5Wx7MSljbN21FIhMXOjzlY73Hx7YrlE
|
||||
Vz9IVARXp1cnr0rZtGx/WfIt8JXwt2FGiXsjb4BkVBfq7unofpgT1QBajYdu9sCO
|
||||
z1C26ctgByXldsHH/7UdS9O/5zMA/WJeHzYxC8GIMt/LSxOOH98FAvDCdnTE8r1b
|
||||
GJb0vEwPUNbjA1rzmx62K/4WsS/aCnxGsAhGFePky/NsuXOqGAXEtbWYhWR6IjJg
|
||||
RELa28RNwT61x67xuc0bz0H0/aaFZ0brIR6td4igyAOb6TxvbT7Esmm3Ua0CAwEA
|
||||
AQKCAf8TS1+/VmRKw5cJaASZQJD8LpA+ZIdgbP9U6Isia/E899CCKaabVREEzp+H
|
||||
OL/TQYOyEwwl6sdZaiRf7m/J+wi3hiTBRnaDZc+CIjtt00PyF4Ibr9G5oKIU6XUM
|
||||
REy4cV+aBWTE3Qs3MAngQuRdOAX9OEfesByLMqOS9Rrm4M7qJBpH20gtJ8iLMdwX
|
||||
iilRx4ruYN5o9MhSZWmYF1sniBAVqmKumCqZUlKAMKDkmavefl4t9/zUPsAglumU
|
||||
T3TeimFQLSfy4Jr2ReruWZk9e1PBTffIXqKq7zWCFEKz60uzwj8qoWJNML30wrtM
|
||||
bu183vHAKgr6SF8zfyAFhkPO7LCbNvApOp+DhaOHzL9fx+T0toFnlgXlQ8YWQAjI
|
||||
Cc6Z8uyKK+R6p4YdhNEB0SUYP8uNNtgmwzhxzf63A5H+XC2r43uveepgN1XtgKbf
|
||||
GuHKwye7FcKs5L+OTMlUEaWXl0FXp1cRUUAxiOdqQggP6UBFfiuCSId5dl9P7+hY
|
||||
FdE7By/3bPK5exdBz5bLcVBowrK67bNfwzFBTZAPhrYQENe45zl0FpWiBEWBHTqY
|
||||
EDxoOqXgzPtrfarT+1MbfOBEQDhZarEc1i8HgUeXUpdC8MR074sN+knMeBBsRw84
|
||||
YwVMXPhPgvJbo12cmnW434aKaw6VXLLfE+uO1F/Q3QI2zNAVAoIBAQDLdaqR3gL/
|
||||
HV4MM3o++1UmN7KZdrCQS9WLRR479/2nSfA3WvCyZW6+qg330RJaDkEO1vyyaaRA
|
||||
c7EIfHy8Ej8Lc9pde0m/1Xl1fZ2p/Tw9e31gAglBHsQlPtrahbTQCwpDXQR5oQwm
|
||||
pk6tfBD83g+hxqTj0a6N/uL8IkviruhI8wniqD690cyPxCDRNdeY3Kf1RGOBatZH
|
||||
VB83f7n6aSrtP0vJreWPx1Nr4LGjS7WcLI7fMeXu+uB45pXOGKbg1B47HQNAArQF
|
||||
732SCOa0E4YpoweDJjIjI/8JSLehqlM+Dfr8RlC2SrjkSPS+vQqLzrZJe4iUqAMd
|
||||
2yBuYD5lQmuzAoIBAQD3FDm8VkBucjxPF3bUZS5gkD8PAzcWBbrU0RVBkA+yZQJF
|
||||
6iZb9nY6owVkzVhtTHM/ejV+cNp6zpMd4MiGZ+yc3OUmskoBNeQIj8WxPYDZc9Bl
|
||||
b1BMdEmy4wz4haZoic8SEfnbPmxh+Rt6y08qkucPWswTJu6+A+kK2cr+Pk2AUkZ7
|
||||
pNgugytLjmFVVaDisgmm/8B6RnFSYuz1yBkgzcBqULNtayUUrTIQTcaAej7Vs/NT
|
||||
umVB5B3gdIGMr1xpu7Z2QgErTzOnpg2rCBDW80Dhl8o6OG6f74u/gSUrbJrPPGYl
|
||||
5bKGpijj9D3SYXXfxa+sOZMgcCs1+PwbxL39Ix0fAoIBAQDHH2XMVMgB/i/pKQhf
|
||||
U4NGYAR/hVXQIyffocmxT0gEzaw3wN0I+5SjLbN18jxPvtuVNnsh8Zo7Kf79GzjI
|
||||
p+LjxoLUMrE++iJhBoujrp+iXJWbvOJpxT6aZSWz8F/BrMximUqj7yYBPYqK532I
|
||||
vZv27H68KJ75gZeMw9QZCq1zl8j490hQZmAZ5A1qM5PJm0sWE9R++Jy2OnJC0tKl
|
||||
bQACKYx22aZuTNosHkA8XQBk1IcPkbpDZW0DZkj+58mLCI59tCtWHk8p7/WpUuTX
|
||||
ILSRU2kqxdsT4UrdPznZEuVdOjmFZRvhDMhfQ7ekZUdJBQoKaMiHFNfYBHl9DNyE
|
||||
JblZAoIBAEZL9NlG2PITgmEmVeK8HuPOZoKI8aVMYAmoqxmKOU0SPAFUSzGi/6RT
|
||||
OXeijOQb+jY3OP0Ocrq6B1Va3PKQottGZdQKqc+KW6Sr8x0oWH6F2ubhMsFt3IIO
|
||||
42PZ8qyPeOC1SJc+PWvqigz3x0Bfp2nQ9XsFequJRUaXDJAlfbtirTcEgZVKMIlA
|
||||
qySyRiH5cZGX9lVTsW41QyHymmOg5nvZFhOthlFJrZLB7hYjsbjvh+1sfN6wXme7
|
||||
/hfe1LYoeBNRWC/QSuwJ7J2an9/oOa91lk3WPHM4nlQQFFk0fx8zGgTyQ0bYA45H
|
||||
sUcn/3d5MTAY+WkjQMgMXP4xjbR1xxUCggEAYzuIlT+zxIocmIFtilzQG5fWCU3Z
|
||||
SYYuP9tRyoRHvTc9N0BojIe34GoQhsqMhlu2u7NlDIDbSQvHtjmi33J0x3Y2uUYy
|
||||
64CcsrNUnN8tOka8jdcshDOhxsUKo08u/+ZNKpTzD4GfgUKvbz4WcZAg8PJTBSjQ
|
||||
XFkboKvTBh926THByobwRL/spPAZTVCydtq8G9cVxaaMq1OQJn2KSPZ/dsmWvCBp
|
||||
zEKwt7g4fFVlQCekJ2tB95LwkyKhjaiqEefyViGM5rtBc4n7KEDKn2L2n+5jBLvD
|
||||
Gb7FwCYpVTtf3futX2xruSie98VhTrDjDB6sSP3iQ9knayk2Okx7o3PoeQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
cmd/internal/storage/testdata/certificates/example.com.crt
vendored
Normal file
1
cmd/internal/storage/testdata/certificates/example.com.crt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
Certificate
|
||||
1
cmd/internal/storage/testdata/certificates/example.com.issuer.crt
vendored
Normal file
1
cmd/internal/storage/testdata/certificates/example.com.issuer.crt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
IssuerCertificate
|
||||
5
cmd/internal/storage/testdata/certificates/example.com.json
vendored
Normal file
5
cmd/internal/storage/testdata/certificates/example.com.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"domain": "example.com",
|
||||
"certUrl": "https://acme.example.org/cert/123",
|
||||
"certStableUrl": "https://acme.example.org/cert/456"
|
||||
}
|
||||
1
cmd/internal/storage/testdata/certificates/example.com.key
vendored
Normal file
1
cmd/internal/storage/testdata/certificates/example.com.key
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
PrivateKey
|
||||
11
cmd/internal/storage/testdata/certificates/example.org.crt
vendored
Normal file
11
cmd/internal/storage/testdata/certificates/example.org.crt
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBkzCCATmgAwIBAgIQZrrrYlA0GzvmKXjrMwLQNDAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYwMDAw
|
||||
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||
NkulPSW9ljKP9q4IRK1lEAp/AvNeWHOcpWgy9c76XkUm05aulJk3Zra1VewY3dq4
|
||||
XjMPIe/YVwmriOjaw1uuE6NvMG0wDgYDVR0PAQH/BAQDAgKEMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKh2BJ5EzUfChYGB
|
||||
wwhWa+8sTLALMBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMAoGCCqGSM49BAMCA0gA
|
||||
MEUCIBbwUdYyeJTRQJBxY8s4IUAr7KhyT2+/WuusIy3+FpsTAiEA+7X2Vikcrerk
|
||||
XWwaukHVTQhirl0SHh2QegsQzM9IzTM=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -19,8 +19,6 @@ func main() {
|
|||
Usage: "Let's Encrypt client written in Go",
|
||||
Version: getVersion(),
|
||||
EnableShellCompletion: true,
|
||||
Flags: cmd.CreateFlags(""),
|
||||
Before: cmd.Before,
|
||||
Commands: cmd.CreateCommands(),
|
||||
}
|
||||
|
||||
|
|
@ -28,8 +26,6 @@ func main() {
|
|||
fmt.Printf("lego version %s %s/%s\n", cmd.Version, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
app.Commands = cmd.CreateCommands()
|
||||
|
||||
err := app.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal("Error", log.ErrorAttr(err))
|
||||
|
|
|
|||
21
cmd/setup.go
21
cmd/setup.go
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
|
|
@ -22,10 +23,8 @@ 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 {
|
||||
func setupClient(cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType) *lego.Client {
|
||||
client := newClient(cmd, account, keyType)
|
||||
|
||||
setupChallenges(cmd, client)
|
||||
|
|
@ -33,15 +32,15 @@ func setupClient(cmd *cli.Command, account *Account, keyType certcrypto.KeyType)
|
|||
return client
|
||||
}
|
||||
|
||||
func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) {
|
||||
func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *storage.AccountsStorage) (*storage.Account, certcrypto.KeyType) {
|
||||
keyType := getKeyType(cmd)
|
||||
privateKey := accountsStorage.GetPrivateKey(keyType)
|
||||
|
||||
var account *Account
|
||||
var account *storage.Account
|
||||
if accountsStorage.ExistsAccountFilePath() {
|
||||
account = accountsStorage.LoadAccount(ctx, privateKey)
|
||||
} else {
|
||||
account = &Account{Email: accountsStorage.GetEmail(), key: privateKey}
|
||||
account = storage.NewAccount(accountsStorage.GetEmail(), privateKey)
|
||||
}
|
||||
|
||||
return account, keyType
|
||||
|
|
@ -123,16 +122,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 {
|
||||
|
|
|
|||
52
cmd/storages.go
Normal file
52
cmd/storages.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CertificatesStorage a certificates' storage.
|
||||
type CertificatesStorage struct {
|
||||
*storage.CertificatesWriter
|
||||
*storage.CertificatesReader
|
||||
}
|
||||
|
||||
// newCertificatesStorage create a new certificates storage.
|
||||
func newCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
|
||||
basePath := cmd.String(flgPath)
|
||||
|
||||
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{
|
||||
CertificatesWriter: writer,
|
||||
CertificatesReader: storage.NewCertificatesReader(basePath),
|
||||
}
|
||||
}
|
||||
|
||||
// newAccountsStorage Creates a new AccountsStorage.
|
||||
func newAccountsStorage(cmd *cli.Command) *storage.AccountsStorage {
|
||||
accountsStorage, err := storage.NewAccountsStorage(storage.AccountsStorageConfig{
|
||||
Email: cmd.String(flgEmail),
|
||||
BasePath: cmd.String(flgPath),
|
||||
Server: cmd.String(flgServer),
|
||||
UserAgent: getUserAgent(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Accounts storage initialization", log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
return accountsStorage
|
||||
}
|
||||
189
docs/data/zz_cli_help.toml
generated
189
docs/data/zz_cli_help.toml
generated
|
|
@ -19,18 +19,40 @@ COMMANDS:
|
|||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
[[command]]
|
||||
title = "lego help run"
|
||||
content = """
|
||||
NAME:
|
||||
lego run - Register an account, then create and install a certificate
|
||||
|
||||
USAGE:
|
||||
lego run
|
||||
|
||||
OPTIONS:
|
||||
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
|
||||
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") [$LEGO_SERVER]
|
||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
||||
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
|
||||
--disable-cn Disable the use of the common name in the CSR.
|
||||
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
|
||||
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
|
||||
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
|
||||
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
|
||||
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
|
||||
--disable-cn Disable the use of the common name in the CSR.
|
||||
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
|
||||
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
|
||||
--tls-skip-verify Skip the TLS verification of the ACME server.
|
||||
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
|
||||
--overall-request-limit int ACME overall requests limit. (default: 18)
|
||||
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
|
||||
--filename string (deprecated) Filename of the generated certificate.
|
||||
--path string Directory to use for storing the data. (default: "./.lego") [$LEGO_PATH]
|
||||
--path string Directory to use for storing the data. [$LEGO_PATH]
|
||||
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
|
||||
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
|
||||
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
|
||||
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
|
||||
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
|
||||
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
|
||||
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
|
||||
|
|
@ -47,42 +69,20 @@ GLOBAL OPTIONS:
|
|||
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
|
||||
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
|
||||
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
|
||||
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
|
||||
--tls-skip-verify Skip the TLS verification of the ACME server.
|
||||
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
|
||||
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
|
||||
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
|
||||
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
|
||||
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
|
||||
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
|
||||
--overall-request-limit int ACME overall requests limit. (default: 18)
|
||||
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
|
||||
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
|
||||
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
|
||||
--not-before time Set the notBefore field in the certificate (RFC3339 format)
|
||||
--not-after time Set the notAfter field in the certificate (RFC3339 format)
|
||||
--private-key string Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.
|
||||
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
|
||||
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
|
||||
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
|
||||
--run-hook string Define a hook. The hook is executed when the certificates are effectively created.
|
||||
--run-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
[[command]]
|
||||
title = "lego help run"
|
||||
content = """
|
||||
NAME:
|
||||
lego run - Register an account, then create and install a certificate
|
||||
|
||||
USAGE:
|
||||
lego run
|
||||
|
||||
OPTIONS:
|
||||
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
|
||||
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
|
||||
--not-before time Set the notBefore field in the certificate (RFC3339 format)
|
||||
--not-after time Set the notAfter field in the certificate (RFC3339 format)
|
||||
--private-key string Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.
|
||||
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
|
||||
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
|
||||
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
|
||||
--run-hook string Define a hook. The hook is executed when the certificates are effectively created.
|
||||
--run-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
[[command]]
|
||||
title = "lego help renew"
|
||||
content = """
|
||||
|
|
@ -93,23 +93,61 @@ USAGE:
|
|||
lego renew
|
||||
|
||||
OPTIONS:
|
||||
--days int The number of days left on a certificate to renew it. (default: 30)
|
||||
--dynamic Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.
|
||||
--ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.
|
||||
--ari-wait-to-renew-duration duration The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
|
||||
--reuse-key Used to indicate you want to reuse your current private key for the new certificate.
|
||||
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
|
||||
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
|
||||
--not-before time Set the notBefore field in the certificate (RFC3339 format)
|
||||
--not-after time Set the notAfter field in the certificate (RFC3339 format)
|
||||
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
|
||||
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
|
||||
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
|
||||
--renew-hook string Define a hook. The hook is executed only when the certificates are effectively renewed.
|
||||
--renew-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
|
||||
--no-random-sleep Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way.
|
||||
--force-cert-domains Check and ensure that the cert's domain list matches those passed in the domains argument.
|
||||
--help, -h show help
|
||||
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
|
||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
||||
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
|
||||
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
|
||||
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
|
||||
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
|
||||
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
|
||||
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
|
||||
--disable-cn Disable the use of the common name in the CSR.
|
||||
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
|
||||
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
|
||||
--tls-skip-verify Skip the TLS verification of the ACME server.
|
||||
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
|
||||
--overall-request-limit int ACME overall requests limit. (default: 18)
|
||||
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
|
||||
--filename string (deprecated) Filename of the generated certificate.
|
||||
--path string Directory to use for storing the data. [$LEGO_PATH]
|
||||
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
|
||||
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
|
||||
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
|
||||
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
|
||||
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
|
||||
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
|
||||
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
|
||||
--http.proxy-header string Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host")
|
||||
--http.webroot string Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
|
||||
--http.memcached-host string [ --http.memcached-host string ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.
|
||||
--http.s3-bucket string Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
|
||||
--tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.
|
||||
--tls.port string Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443")
|
||||
--tls.delay duration Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge. (default: 0s)
|
||||
--dns string Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.
|
||||
--dns.disable-cp (deprecated) use dns.propagation-disable-ans instead.
|
||||
--dns.propagation-disable-ans By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.
|
||||
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
|
||||
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
|
||||
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
|
||||
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
|
||||
--days int The number of days left on a certificate to renew it. (default: 30)
|
||||
--dynamic Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.
|
||||
--ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.
|
||||
--ari-wait-to-renew-duration duration The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
|
||||
--reuse-key Used to indicate you want to reuse your current private key for the new certificate.
|
||||
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
|
||||
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
|
||||
--not-before time Set the notBefore field in the certificate (RFC3339 format)
|
||||
--not-after time Set the notAfter field in the certificate (RFC3339 format)
|
||||
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
|
||||
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
|
||||
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
|
||||
--renew-hook string Define a hook. The hook is executed only when the certificates are effectively renewed.
|
||||
--renew-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
|
||||
--no-random-sleep Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way.
|
||||
--force-cert-domains Check and ensure that the cert's domain list matches those passed in the domains argument.
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
[[command]]
|
||||
|
|
@ -122,9 +160,47 @@ USAGE:
|
|||
lego revoke
|
||||
|
||||
OPTIONS:
|
||||
--keep, -k Keep the certificates after the revocation instead of archiving them.
|
||||
--reason uint Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. Valid values are: 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged), 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL), 9 (privilegeWithdrawn), or 10 (aACompromise). (default: 0)
|
||||
--help, -h show help
|
||||
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
|
||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
||||
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
|
||||
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
|
||||
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
|
||||
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
|
||||
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
|
||||
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
|
||||
--disable-cn Disable the use of the common name in the CSR.
|
||||
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
|
||||
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
|
||||
--tls-skip-verify Skip the TLS verification of the ACME server.
|
||||
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
|
||||
--overall-request-limit int ACME overall requests limit. (default: 18)
|
||||
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
|
||||
--filename string (deprecated) Filename of the generated certificate.
|
||||
--path string Directory to use for storing the data. [$LEGO_PATH]
|
||||
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
|
||||
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
|
||||
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
|
||||
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
|
||||
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
|
||||
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
|
||||
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
|
||||
--http.proxy-header string Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host")
|
||||
--http.webroot string Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
|
||||
--http.memcached-host string [ --http.memcached-host string ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.
|
||||
--http.s3-bucket string Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
|
||||
--tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.
|
||||
--tls.port string Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443")
|
||||
--tls.delay duration Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge. (default: 0s)
|
||||
--dns string Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.
|
||||
--dns.disable-cp (deprecated) use dns.propagation-disable-ans instead.
|
||||
--dns.propagation-disable-ans By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.
|
||||
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
|
||||
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
|
||||
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
|
||||
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
|
||||
--keep, -k Keep the certificates after the revocation instead of archiving them.
|
||||
--reason uint Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. Valid values are: 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged), 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL), 9 (privilegeWithdrawn), or 10 (aACompromise). (default: 0)
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
[[command]]
|
||||
|
|
@ -138,7 +214,8 @@ USAGE:
|
|||
|
||||
OPTIONS:
|
||||
--accounts, -a Display accounts.
|
||||
--names, -n Display certificate common names only.
|
||||
--names, -n Display certificate names only.
|
||||
--path string Directory to use for storing the data. [$LEGO_PATH]
|
||||
--help, -h show help
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -66,13 +66,14 @@ func TestChallengeHTTP_Run(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-d", testDomain1,
|
||||
"--http",
|
||||
"--http.port", ":5002",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -82,13 +83,14 @@ func TestChallengeTLS_Run_Domains(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-d", testDomain1,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -98,13 +100,14 @@ func TestChallengeTLS_Run_IP(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-d", "127.0.0.1",
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -116,13 +119,14 @@ func TestChallengeTLS_Run_CSR(t *testing.T) {
|
|||
csrPath := createTestCSRFile(t, true)
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-csr", csrPath,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -134,13 +138,14 @@ func TestChallengeTLS_Run_CSR_PEM(t *testing.T) {
|
|||
csrPath := createTestCSRFile(t, false)
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-csr", csrPath,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -150,6 +155,7 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
|
|
@ -157,12 +163,13 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) {
|
|||
"-d", testDomain3,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = load.RunLego(t.Context(),
|
||||
"revoke",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
|
|
@ -179,25 +186,27 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-d", testDomain4,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = load.RunLego(t.Context(),
|
||||
"revoke",
|
||||
"-m", testEmail1,
|
||||
"--accept-tos",
|
||||
"-s", "https://localhost:14000/dir",
|
||||
"-d", testDomain4,
|
||||
"--tls",
|
||||
"--tls.port", ":5001",
|
||||
"revoke")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ func TestChallengeDNS_Run(t *testing.T) {
|
|||
loader.CleanLegoFiles(t.Context())
|
||||
|
||||
err := load.RunLego(t.Context(),
|
||||
"run",
|
||||
"--accept-tos",
|
||||
"--dns", "exec",
|
||||
"--dns.resolvers", ":8053",
|
||||
|
|
@ -66,7 +67,7 @@ func TestChallengeDNS_Run(t *testing.T) {
|
|||
"-s", "https://localhost:15000/dir",
|
||||
"-d", testDomain2,
|
||||
"-d", testDomain1,
|
||||
"run")
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,14 +86,12 @@ func generate(ctx context.Context) error {
|
|||
|
||||
// createStubApp Construct cli app, very similar to cmd/lego/main.go.
|
||||
// Notable differences:
|
||||
// - substitute "." for CWD in default config path, as the user will very likely see a different path
|
||||
// - do not include version information, because we're likely running against a snapshot
|
||||
// - skip DNS help and provider list, as initialization takes time, and we don't generate `lego dns --help` here.
|
||||
func createStubApp() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "lego",
|
||||
Usage: "Let's Encrypt client written in Go",
|
||||
Flags: cmd.CreateFlags("./.lego"),
|
||||
Commands: cmd.CreateCommands(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue