diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index e12c7762a..e070193c8 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -11,14 +11,10 @@ import ( "github.com/go-acme/lego/v5/certcrypto" "github.com/go-acme/lego/v5/cmd/internal/storage" + "github.com/go-acme/lego/v5/log" "github.com/urfave/cli/v3" ) -const ( - flgAccounts = "accounts" - flgNames = "names" -) - func createList() *cli.Command { return &cli.Command{ Name: "list", @@ -28,22 +24,6 @@ func createList() *cli.Command { } } -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), - } -} - func list(ctx context.Context, cmd *cli.Command) error { if cmd.Bool(flgAccounts) && !cmd.Bool(flgNames) { if err := listAccount(ctx, cmd); err != nil { @@ -111,7 +91,10 @@ func listCertificates(_ context.Context, cmd *cli.Command) error { } func listAccount(_ context.Context, cmd *cli.Command) error { - accountsStorage := newAccountsStorage(cmd) + accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd)) + if err != nil { + log.Fatal("Accounts storage initialization", log.ErrorAttr(err)) + } matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json")) if err != nil { diff --git a/cmd/cmd_register.go b/cmd/cmd_register.go new file mode 100644 index 000000000..395c5bbb5 --- /dev/null +++ b/cmd/cmd_register.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "bufio" + "context" + "fmt" + "log/slog" + "os" + "strings" + + "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" +) + +// TODO(ldez): add register command. + +const rootPathWarningMessage = `!!!! HEADS UP !!!! + +Your account credentials have been saved in your +configuration directory at "%s". + +You should make a secure backup of this folder now. This +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. +` + +func registerAccount(ctx context.Context, cmd *cli.Command, client *lego.Client) (*registration.Resource, error) { + accepted := handleTOS(cmd, client) + if !accepted { + log.Fatal("You did not accept the TOS. Unable to proceed.") + } + + if cmd.Bool(flgEAB) { + kid := cmd.String(flgKID) + hmacEncoded := cmd.String(flgHMAC) + + if kid == "" || hmacEncoded == "" { + log.Fatal(fmt.Sprintf("Requires arguments --%s and --%s.", flgKID, flgHMAC)) + } + + return client.Registration.RegisterWithExternalAccountBinding(ctx, registration.RegisterEABOptions{ + TermsOfServiceAgreed: accepted, + Kid: kid, + HmacEncoded: hmacEncoded, + }) + } + + return client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true}) +} + +func handleTOS(cmd *cli.Command, client *lego.Client) bool { + // Check for a global accept override + if cmd.Bool(flgAcceptTOS) { + return true + } + + reader := bufio.NewReader(os.Stdin) + + log.Warn("Please review the TOS", slog.String("url", client.GetToSURL())) + + for { + fmt.Println("Do you accept the TOS? Y/n") + + text, err := reader.ReadString('\n') + if err != nil { + log.Fatal("Could not read from the console", log.ErrorAttr(err)) + } + + text = strings.Trim(text, "\r\n") + switch text { + case "", "y", "Y": + return true + case "n", "N": + return false + default: + fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.") + } + } +} diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index 3efbef272..fe5b189a6 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -25,19 +25,6 @@ import ( "github.com/urfave/cli/v3" ) -// Flag names. -const ( - flgRenewDays = "days" - flgRenewDynamic = "dynamic" - flgARIDisable = "ari-disable" - flgARIWaitToRenewDuration = "ari-wait-to-renew-duration" - flgReuseKey = "reuse-key" - flgRenewHook = "renew-hook" - flgRenewHookTimeout = "renew-hook-timeout" - flgNoRandomSleep = "no-random-sleep" - flgForceCertDomains = "force-cert-domains" -) - func createRenew() *cli.Command { return &cli.Command{ Name: "renew", @@ -66,102 +53,24 @@ func createRenew() *cli.Command { } } -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)) + accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd)) + if err != nil { + log.Fatal("Accounts storage initialization", log.ErrorAttr(err)) + } + + keyType := getKeyType(cmd) + + account := setupAccount(ctx, keyType, accountsStorage) 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) - - bundle := !cmd.Bool(flgNoBundle) + certsStorage, err := storage.NewCertificatesStorage(newCertificatesWriterConfig(cmd)) + if err != nil { + log.Fatal("Certificates storage", log.ErrorAttr(err)) + } meta := map[string]string{ hook.EnvAccountEmail: account.Email, @@ -169,14 +78,14 @@ func renew(ctx context.Context, cmd *cli.Command) error { // CSR if cmd.IsSet(flgCSR) { - return renewForCSR(ctx, cmd, account, keyType, certsStorage, bundle, meta) + return renewForCSR(ctx, cmd, account, keyType, certsStorage, meta) } // Domains - return renewForDomains(ctx, cmd, account, keyType, certsStorage, bundle, meta) + return renewForDomains(ctx, cmd, account, keyType, certsStorage, meta) } -func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.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 *storage.CertificatesStorage, meta map[string]string) error { domains := cmd.StringSlice(flgDomains) domain := domains[0] @@ -185,7 +94,10 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Acc // as web servers would not be able to work with a combined file. certificates, err := certsStorage.ReadCertificate(domain, storage.ExtCert) if err != nil { - log.Fatal("Error while loading the certificate.", log.DomainAttr(domain), log.ErrorAttr(err)) + log.Fatal("Error while loading the certificate.", + log.DomainAttr(domain), + log.ErrorAttr(err), + ) } cert := certificates[0] @@ -200,7 +112,9 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Acc if !cmd.Bool(flgARIDisable) { client = setupClient(cmd, account, keyType) - ariRenewalTime = getARIRenewalTime(ctx, cmd, cert, domain, client) + willingToSleep := cmd.Duration(flgARIWaitToRenewDuration) + + ariRenewalTime = getARIRenewalTime(ctx, willingToSleep, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() @@ -276,17 +190,9 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Acc renewalDomains = merge(certDomains, domains) } - request := certificate.ObtainRequest{ - Domains: renewalDomains, - PrivateKey: privateKey, - MustStaple: cmd.Bool(flgMustStaple), - NotBefore: cmd.Timestamp(flgNotBefore), - NotAfter: cmd.Timestamp(flgNotAfter), - Bundle: bundle, - PreferredChain: cmd.String(flgPreferredChain), - Profile: cmd.String(flgProfile), - AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), - } + request := newObtainRequest(cmd, renewalDomains) + + request.PrivateKey = privateKey if replacesCertID != "" { request.ReplacesCertID = replacesCertID @@ -301,17 +207,16 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Acc certsStorage.SaveResource(certRes) - addPathToMetadata(meta, domain, certRes, certsStorage) + hook.AddPathToMetadata(meta, certRes.Domain, certRes, certsStorage) return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta) } -func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.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 *storage.CertificatesStorage, meta map[string]string) error { csr, err := readCSRFile(cmd.String(flgCSR)) if err != nil { log.Fatal("Could not read CSR file.", - slog.String("flag", flgCSR), - slog.String("filepath", cmd.String(flgCSR)), + slog.String(flgCSR, cmd.String(flgCSR)), log.ErrorAttr(err), ) } @@ -344,7 +249,9 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account if !cmd.Bool(flgARIDisable) { client = setupClient(cmd, account, keyType) - ariRenewalTime = getARIRenewalTime(ctx, cmd, cert, domain, client) + willingToSleep := cmd.Duration(flgARIWaitToRenewDuration) + + ariRenewalTime = getARIRenewalTime(ctx, willingToSleep, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() @@ -353,7 +260,8 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account log.Info("Sleeping until renewal time", log.DomainAttr(domain), slog.Duration("sleep", ariRenewalTime.Sub(now)), - slog.Time("renewalTime", *ariRenewalTime)) + slog.Time("renewalTime", *ariRenewalTime), + ) time.Sleep(ariRenewalTime.Sub(now)) } } @@ -379,15 +287,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account slog.Int("hoursRemaining", int(timeLeft.Hours())), ) - request := certificate.ObtainForCSRRequest{ - CSR: csr, - NotBefore: cmd.Timestamp(flgNotBefore), - NotAfter: cmd.Timestamp(flgNotAfter), - Bundle: bundle, - PreferredChain: cmd.String(flgPreferredChain), - Profile: cmd.String(flgProfile), - AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), - } + request := newObtainForCSRRequest(cmd, csr) if replacesCertID != "" { request.ReplacesCertID = replacesCertID @@ -400,7 +300,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account certsStorage.SaveResource(certRes) - addPathToMetadata(meta, domain, certRes, certsStorage) + hook.AddPathToMetadata(meta, domain, certRes, certsStorage) return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta) } @@ -453,7 +353,7 @@ func needRenewalDynamic(x509Cert *x509.Certificate, domain string, now time.Time } // getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint. -func getARIRenewalTime(ctx context.Context, cmd *cli.Command, cert *x509.Certificate, domain string, client *lego.Client) *time.Time { +func getARIRenewalTime(ctx context.Context, willingToSleep time.Duration, cert *x509.Certificate, domain string, client *lego.Client) *time.Time { if cert.IsCA { log.Fatal("Certificate bundle starts with a CA certificate.", log.DomainAttr(domain)) } @@ -479,7 +379,7 @@ func getARIRenewalTime(ctx context.Context, cmd *cli.Command, cert *x509.Certifi now := time.Now().UTC() - renewalTime := renewalInfo.ShouldRenewAt(now, cmd.Duration(flgARIWaitToRenewDuration)) + renewalTime := renewalInfo.ShouldRenewAt(now, willingToSleep) if renewalTime == nil { log.Info("acme: renewalInfo endpoint indicates that renewal is not needed.", log.DomainAttr(domain)) return nil diff --git a/cmd/cmd_revoke.go b/cmd/cmd_revoke.go index dc0bc5cf4..2508f835b 100644 --- a/cmd/cmd_revoke.go +++ b/cmd/cmd_revoke.go @@ -4,18 +4,11 @@ import ( "context" "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" ) -// Flag names. -const ( - flgKeep = "keep" - flgReason = "reason" -) - func createRevoke() *cli.Command { return &cli.Command{ Name: "revoke", @@ -25,32 +18,15 @@ func createRevoke() *cli.Command { } } -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)) + accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd)) + if err != nil { + log.Fatal("Accounts storage initialization", log.ErrorAttr(err)) + } + + keyType := getKeyType(cmd) + + account := setupAccount(ctx, keyType, accountsStorage) if account.Registration == nil { log.Fatal("Account is not registered. Use 'run' to register a new account.", slog.String("email", account.Email)) @@ -58,7 +34,11 @@ func revoke(ctx context.Context, cmd *cli.Command) error { client := newClient(cmd, account, keyType) - certsStorage := newCertificatesStorage(cmd) + certsStorage, err := storage.NewCertificatesStorage(newCertificatesWriterConfig(cmd)) + if err != nil { + log.Fatal("Certificates storage", log.ErrorAttr(err)) + } + certsStorage.CreateRootFolder() for _, domain := range cmd.StringSlice(flgDomains) { diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index b607e57b0..93d180f02 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -1,13 +1,9 @@ package cmd import ( - "bufio" "context" + "crypto/x509" "fmt" - "log/slog" - "os" - "strings" - "time" "github.com/go-acme/lego/v5/certificate" "github.com/go-acme/lego/v5/cmd/internal/hook" @@ -18,20 +14,6 @@ import ( "github.com/urfave/cli/v3" ) -// Flag names. -const ( - flgNoBundle = "no-bundle" - flgMustStaple = "must-staple" - flgNotBefore = "not-before" - flgNotAfter = "not-after" - flgPrivateKey = "private-key" - flgPreferredChain = "preferred-chain" - flgProfile = "profile" - flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations" - flgRunHook = "run-hook" - flgRunHookTimeout = "run-hook-timeout" -) - func createRun() *cli.Command { return &cli.Command{ Name: "run", @@ -56,84 +38,22 @@ func createRun() *cli.Command { } } -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 !!!! - -Your account credentials have been saved in your -configuration directory at "%s". - -You should make a secure backup of this folder now. This -configuration directory will also contain private keys -generated by lego and certificates obtained from the ACME -server. Making regular backups of this folder is ideal. -` - func run(ctx context.Context, cmd *cli.Command) error { - accountsStorage := newAccountsStorage(cmd) + accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd)) + if err != nil { + log.Fatal("Accounts storage initialization", log.ErrorAttr(err)) + } - account, keyType := setupAccount(ctx, cmd, accountsStorage) + keyType := getKeyType(cmd) + + account := setupAccount(ctx, keyType, accountsStorage) client := setupClient(cmd, account, keyType) if account.Registration == nil { - reg, err := register(ctx, cmd, client) + var reg *registration.Resource + + reg, err = registerAccount(ctx, cmd, client) if err != nil { log.Fatal("Could not complete registration.", log.ErrorAttr(err)) } @@ -146,7 +66,11 @@ func run(ctx context.Context, cmd *cli.Command) error { fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath()) } - certsStorage := newCertificatesStorage(cmd) + certsStorage, err := storage.NewCertificatesStorage(newCertificatesWriterConfig(cmd)) + if err != nil { + log.Fatal("Certificates storage", log.ErrorAttr(err)) + } + certsStorage.CreateRootFolder() cert, err := obtainCertificate(ctx, cmd, client) @@ -162,82 +86,19 @@ func run(ctx context.Context, cmd *cli.Command) error { hook.EnvAccountEmail: account.Email, } - addPathToMetadata(meta, cert.Domain, cert, certsStorage) + hook.AddPathToMetadata(meta, cert.Domain, cert, certsStorage) return hook.Launch(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta) } -func handleTOS(cmd *cli.Command, client *lego.Client) bool { - // Check for a global accept override - if cmd.Bool(flgAcceptTOS) { - return true - } - - reader := bufio.NewReader(os.Stdin) - - log.Warn("Please review the TOS", slog.String("url", client.GetToSURL())) - - for { - fmt.Println("Do you accept the TOS? Y/n") - - text, err := reader.ReadString('\n') - if err != nil { - log.Fatal("Could not read from the console", log.ErrorAttr(err)) - } - - text = strings.Trim(text, "\r\n") - switch text { - case "", "y", "Y": - return true - case "n", "N": - return false - default: - fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.") - } - } -} - -func register(ctx context.Context, cmd *cli.Command, client *lego.Client) (*registration.Resource, error) { - accepted := handleTOS(cmd, client) - if !accepted { - log.Fatal("You did not accept the TOS. Unable to proceed.") - } - - if cmd.Bool(flgEAB) { - kid := cmd.String(flgKID) - hmacEncoded := cmd.String(flgHMAC) - - if kid == "" || hmacEncoded == "" { - log.Fatal(fmt.Sprintf("Requires arguments --%s and --%s.", flgKID, flgHMAC)) - } - - return client.Registration.RegisterWithExternalAccountBinding(ctx, registration.RegisterEABOptions{ - TermsOfServiceAgreed: accepted, - Kid: kid, - HmacEncoded: hmacEncoded, - }) - } - - return client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true}) -} - func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Client) (*certificate.Resource, error) { - bundle := !cmd.Bool(flgNoBundle) - domains := cmd.StringSlice(flgDomains) + if len(domains) > 0 { // obtain a certificate, generating a new private key - request := certificate.ObtainRequest{ - Domains: domains, - MustStaple: cmd.Bool(flgMustStaple), - NotBefore: cmd.Timestamp(flgNotBefore), - NotAfter: cmd.Timestamp(flgNotAfter), - Bundle: bundle, - PreferredChain: cmd.String(flgPreferredChain), - Profile: cmd.String(flgProfile), - AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), - } + request := newObtainRequest(cmd, domains) + // TODO(ldez): factorize? if cmd.IsSet(flgPrivateKey) { var err error @@ -257,16 +118,9 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien } // obtain a certificate for this CSR - request := certificate.ObtainForCSRRequest{ - CSR: csr, - NotBefore: cmd.Timestamp(flgNotBefore), - NotAfter: cmd.Timestamp(flgNotAfter), - Bundle: bundle, - PreferredChain: cmd.String(flgPreferredChain), - Profile: cmd.String(flgProfile), - AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), - } + request := newObtainForCSRRequest(cmd, csr) + // TODO(ldez): factorize? if cmd.IsSet(flgPrivateKey) { var err error @@ -278,3 +132,28 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien return client.Certificate.ObtainForCSR(ctx, request) } + +func newObtainRequest(cmd *cli.Command, domains []string) certificate.ObtainRequest { + return certificate.ObtainRequest{ + Domains: domains, + MustStaple: cmd.Bool(flgMustStaple), + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), + Bundle: !cmd.Bool(flgNoBundle), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), + } +} + +func newObtainForCSRRequest(cmd *cli.Command, csr *x509.CertificateRequest) certificate.ObtainForCSRRequest { + return certificate.ObtainForCSRRequest{ + CSR: csr, + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), + Bundle: !cmd.Bool(flgNoBundle), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), + } +} diff --git a/cmd/flags.go b/cmd/flags.go index 0f122e32e..b57fa1dbc 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -4,7 +4,9 @@ import ( "fmt" "os" "path/filepath" + "time" + "github.com/go-acme/lego/v5/acme" "github.com/go-acme/lego/v5/certificate" "github.com/go-acme/lego/v5/cmd/internal/storage" "github.com/go-acme/lego/v5/lego" @@ -17,15 +19,25 @@ const ( flgDomains = "domains" flgAcceptTOS = "accept-tos" flgEmail = "email" - flgCSR = "csr" flgEAB = "eab" flgKID = "kid" flgHMAC = "hmac" ) +// Flag names related to Obtain certificates. +const ( + flgCSR = "csr" + flgNoBundle = "no-bundle" + flgMustStaple = "must-staple" + flgNotBefore = "not-before" + flgNotAfter = "not-after" + flgPreferredChain = "preferred-chain" + flgProfile = "profile" + flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations" +) + // Flag names related to the output. const ( - flgFilename = "filename" flgPath = "path" flgPEM = "pem" flgPFX = "pfx" @@ -74,6 +86,43 @@ const ( flgDNSTimeout = "dns-timeout" ) +// Flags names related to hooks. +const ( + flgRenewHook = "renew-hook" + flgRenewHookTimeout = "renew-hook-timeout" + + flgRunHook = "run-hook" + flgRunHookTimeout = "run-hook-timeout" +) + +// Flag names related to the specific run command. +const ( + flgPrivateKey = "private-key" +) + +// Flag names related to the specific renew command. +const ( + flgRenewDays = "days" + flgRenewDynamic = "dynamic" + flgARIDisable = "ari-disable" + flgARIWaitToRenewDuration = "ari-wait-to-renew-duration" + flgReuseKey = "reuse-key" + flgNoRandomSleep = "no-random-sleep" + flgForceCertDomains = "force-cert-domains" +) + +// Flag names related to the specific revoke command. +const ( + flgKeep = "keep" + flgReason = "reason" +) + +// Flag names related to the list command. +const ( + flgAccounts = "accounts" + flgNames = "names" +) + // Environment variable names. const ( envEAB = "LEGO_EAB" @@ -132,6 +181,16 @@ func CreateACMEClientFlags() []cli.Flag { } } +func CreateChallengesFlags() []cli.Flag { + var flags []cli.Flag + + flags = append(flags, CreateHTTPChallengeFlags()...) + flags = append(flags, CreateTLSChallengeFlags()...) + flags = append(flags, CreateDNSChallengeFlags()...) + + return flags +} + func CreateHTTPChallengeFlags() []cli.Flag { return []cli.Flag{ &cli.BoolFlag{ @@ -227,10 +286,6 @@ func CreateDNSChallengeFlags() []cli.Flag { func CreateOutputFlags() []cli.Flag { return []cli.Flag{ - &cli.StringFlag{ - Name: flgFilename, - Usage: "(deprecated) Filename of the generated certificate.", - }, CreatePathFlag(true), &cli.BoolFlag{ Name: flgPEM, @@ -256,27 +311,6 @@ func CreateOutputFlags() []cli.Flag { } } -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 - }, - Required: true, - } -} - func CreateAccountFlags() []cli.Flag { return []cli.Flag{ &cli.BoolFlag{ @@ -290,11 +324,6 @@ func CreateAccountFlags() []cli.Flag { 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), @@ -313,25 +342,208 @@ func CreateAccountFlags() []cli.Flag { } } -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.", +func CreateObtainFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: flgCSR, + Aliases: []string{"c"}, + Usage: "Certificate signing request filename, if an external CSR is to be used.", + }, + &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.", }, } +} +func CreateHookFlags(name, timeoutName string) []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: name, + Usage: "Define a hook. The hook is executed only when the certificates are effectively created/renewed.", + }, + &cli.DurationFlag{ + Name: timeoutName, + Usage: "Define the timeout for the hook execution.", + Value: 2 * time.Minute, + }, + } +} + +func CreateBaseFlags() []cli.Flag { + var flags []cli.Flag + + flags = append(flags, CreateDomainFlag()) 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 } +func createRunFlags() []cli.Flag { + flags := CreateBaseFlags() + + flags = append(flags, CreateChallengesFlags()...) + flags = append(flags, CreateObtainFlags()...) + flags = append(flags, CreateHookFlags(flgRunHook, flgRunHookTimeout)...) + + flags = append(flags, + &cli.StringFlag{ + Name: flgPrivateKey, + Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.", + }, + ) + + return flags +} + +func createRenewFlags() []cli.Flag { + flags := CreateBaseFlags() + + flags = append(flags, CreateChallengesFlags()...) + flags = append(flags, CreateObtainFlags()...) + flags = append(flags, CreateHookFlags(flgRenewHook, flgRenewHookTimeout)...) + + 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: 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 createRevokeFlags() []cli.Flag { + flags := CreateBaseFlags() + + 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 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), + } +} + +func CreateDomainFlag() cli.Flag { + return &cli.StringSliceFlag{ + Name: flgDomains, + Aliases: []string{"d"}, + Usage: "Add a domain to the process. Can be specified multiple times or use comma as a separator.", + } +} + +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 + }, + Required: true, + } +} + // defaultPathValueSource gets the default path based on the current working directory. // The field value is only here because clihelp/generator. type defaultPathValueSource struct{} diff --git a/cmd/hook.go b/cmd/hook.go deleted file mode 100644 index 4e7931e73..000000000 --- a/cmd/hook.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "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" -) - -func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) { - 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[hook.EnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtIssuer) - } - - if certsStorage.IsPEM() { - meta[hook.EnvCertPEMPath] = certsStorage.GetFileName(domain, storage.ExtPEM) - } - - if certsStorage.IsPFX() { - meta[hook.EnvCertPFXPath] = certsStorage.GetFileName(domain, storage.ExtPFX) - } -} diff --git a/cmd/internal/hook/hook.go b/cmd/internal/hook/hook.go index 8a1aaf835..79988905d 100644 --- a/cmd/internal/hook/hook.go +++ b/cmd/internal/hook/hook.go @@ -9,6 +9,9 @@ import ( "os/exec" "strings" "time" + + "github.com/go-acme/lego/v5/certificate" + "github.com/go-acme/lego/v5/cmd/internal/storage" ) const ( @@ -82,3 +85,22 @@ func metaToEnv(meta map[string]string) []string { return envs } + +// AddPathToMetadata adds information about the certificate to the metadata map. +func AddPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *storage.CertificatesStorage) { + meta[EnvCertDomain] = domain + meta[EnvCertPath] = certsStorage.GetFileName(domain, storage.ExtCert) + meta[EnvCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtKey) + + if certRes.IssuerCertificate != nil { + meta[EnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtIssuer) + } + + if certsStorage.IsPEM() { + meta[EnvCertPEMPath] = certsStorage.GetFileName(domain, storage.ExtPEM) + } + + if certsStorage.IsPFX() { + meta[EnvCertPFXPath] = certsStorage.GetFileName(domain, storage.ExtPFX) + } +} diff --git a/cmd/internal/storage/account.go b/cmd/internal/storage/account.go index 1e9bb62f0..24cd9c2ab 100644 --- a/cmd/internal/storage/account.go +++ b/cmd/internal/storage/account.go @@ -24,7 +24,7 @@ func (a *Account) GetEmail() string { return a.Email } -// GetPrivateKey returns the private RSA account key. +// GetPrivateKey returns the private account key. func (a *Account) GetPrivateKey() crypto.PrivateKey { return a.key } diff --git a/cmd/internal/storage/accounts_storage.go b/cmd/internal/storage/accounts.go similarity index 100% rename from cmd/internal/storage/accounts_storage.go rename to cmd/internal/storage/accounts.go diff --git a/cmd/internal/storage/accounts_storage_test.go b/cmd/internal/storage/accounts_test.go similarity index 100% rename from cmd/internal/storage/accounts_storage_test.go rename to cmd/internal/storage/accounts_test.go diff --git a/cmd/internal/storage/certificates.go b/cmd/internal/storage/certificates.go index 42d75cb67..7b19155e1 100644 --- a/cmd/internal/storage/certificates.go +++ b/cmd/internal/storage/certificates.go @@ -1,8 +1,13 @@ package storage import ( + "fmt" "os" "path/filepath" + "strings" + + "github.com/go-acme/lego/v5/log" + "golang.org/x/net/idna" ) const ( @@ -19,12 +24,23 @@ const ( baseArchivesFolderName = "archives" ) -func getCertificatesRootPath(basePath string) string { - return filepath.Join(basePath, baseCertificatesFolderName) +// CertificatesStorage a certificates' storage. +type CertificatesStorage struct { + *CertificatesWriter + *CertificatesReader } -func getCertificatesArchivePath(basePath string) string { - return filepath.Join(basePath, baseArchivesFolderName) +// NewCertificatesStorage create a new certificates storage. +func NewCertificatesStorage(config CertificatesWriterConfig) (*CertificatesStorage, error) { + writer, err := NewCertificatesWriter(config) + if err != nil { + return nil, fmt.Errorf("certificates storage writer: %w", err) + } + + return &CertificatesStorage{ + CertificatesWriter: writer, + CertificatesReader: NewCertificatesReader(config.BasePath), + }, nil } func CreateNonExistingFolder(path string) error { @@ -36,3 +52,24 @@ func CreateNonExistingFolder(path string) error { return nil } + +func getCertificatesRootPath(basePath string) string { + return filepath.Join(basePath, baseCertificatesFolderName) +} + +func getCertificatesArchivePath(basePath string) string { + return filepath.Join(basePath, baseArchivesFolderName) +} + +// 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 +} diff --git a/cmd/internal/storage/certificates_reader.go b/cmd/internal/storage/certificates_reader.go index 24d0d5459..f602c73a3 100644 --- a/cmd/internal/storage/certificates_reader.go +++ b/cmd/internal/storage/certificates_reader.go @@ -6,12 +6,10 @@ import ( "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 { @@ -78,16 +76,3 @@ func (s *CertificatesReader) ReadCertificate(domain, extension string) ([]*x509. // 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 -} diff --git a/cmd/internal/storage/certificates_writer.go b/cmd/internal/storage/certificates_writer.go index 433db84b9..2da85eceb 100644 --- a/cmd/internal/storage/certificates_writer.go +++ b/cmd/internal/storage/certificates_writer.go @@ -29,8 +29,6 @@ type CertificatesWriterConfig struct { PFX bool PFXFormat string PFXPassword string - - Filename string // TODO(ldez): remove } // CertificatesWriter a writer of certificate files. @@ -55,8 +53,6 @@ type CertificatesWriter struct { pfx bool pfxFormat string pfxPassword string - - filename string // TODO(ldez): remove } // NewCertificatesWriter create a new certificates storage writer. @@ -76,7 +72,6 @@ func NewCertificatesWriter(config CertificatesWriterConfig) (*CertificatesWriter pfx: config.PFX, pfxPassword: config.PFXPassword, pfxFormat: config.PFXFormat, - filename: config.Filename, }, nil } @@ -247,14 +242,7 @@ func (s *CertificatesWriter) writePFXFile(domain string, certRes *certificate.Re } func (s *CertificatesWriter) writeFile(domain, extension string, data []byte) error { - var baseFileName string - if s.filename != "" { - baseFileName = s.filename - } else { - baseFileName = sanitizedDomain(domain) - } - - filePath := filepath.Join(s.rootPath, baseFileName+extension) + filePath := filepath.Join(s.rootPath, sanitizedDomain(domain)+extension) log.Info("Writing file.", slog.String("filepath", filePath)) diff --git a/cmd/setup.go b/cmd/setup.go index 75e8c1de2..b5a06bb5d 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -32,21 +32,30 @@ func setupClient(cmd *cli.Command, account *storage.Account, keyType certcrypto. return client } -func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *storage.AccountsStorage) (*storage.Account, certcrypto.KeyType) { - keyType := getKeyType(cmd) +func setupAccount(ctx context.Context, keyType certcrypto.KeyType, accountsStorage *storage.AccountsStorage) *storage.Account { privateKey := accountsStorage.GetPrivateKey(keyType) - var account *storage.Account if accountsStorage.ExistsAccountFilePath() { - account = accountsStorage.LoadAccount(ctx, privateKey) - } else { - account = storage.NewAccount(accountsStorage.GetEmail(), privateKey) + return accountsStorage.LoadAccount(ctx, privateKey) } - return account, keyType + return storage.NewAccount(accountsStorage.GetEmail(), privateKey) } func newClient(cmd *cli.Command, acc registration.User, keyType certcrypto.KeyType) *lego.Client { + client, err := lego.NewClient(newClientConfig(cmd, acc, keyType)) + if err != nil { + log.Fatal("Could not create client.", log.ErrorAttr(err)) + } + + if client.GetExternalAccountRequired() && !cmd.IsSet(flgEAB) { // TODO(ldez): handle this flag. + log.Fatal(fmt.Sprintf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC)) + } + + return client +} + +func newClientConfig(cmd *cli.Command, acc registration.User, keyType certcrypto.KeyType) *lego.Config { config := lego.NewConfig(acc) config.CADirURL = cmd.String(flgServer) @@ -83,16 +92,7 @@ func newClient(cmd *cli.Command, acc registration.User, keyType certcrypto.KeyTy config.HTTPClient = retryClient.StandardClient() - client, err := lego.NewClient(config) - if err != nil { - log.Fatal("Could not create client.", log.ErrorAttr(err)) - } - - if client.GetExternalAccountRequired() && !cmd.IsSet(flgEAB) { - log.Fatal(fmt.Sprintf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC)) - } - - return client + return config } // getKeyType the type from which private keys should be generated. diff --git a/cmd/storages.go b/cmd/storages.go index a6b9c2189..32460c38c 100644 --- a/cmd/storages.go +++ b/cmd/storages.go @@ -2,51 +2,24 @@ 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, +func newCertificatesWriterConfig(cmd *cli.Command) storage.CertificatesWriterConfig { + return storage.CertificatesWriterConfig{ + BasePath: cmd.String(flgPath), 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{ +func newAccountsStorageConfig(cmd *cli.Command) storage.AccountsStorageConfig { + return 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 } diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index ceb287d3b..8a2727630 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -32,10 +32,9 @@ USAGE: lego run OPTIONS: - --domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times. + --domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times or use comma as a separator. --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] @@ -47,7 +46,6 @@ OPTIONS: --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] @@ -70,16 +68,17 @@ OPTIONS: --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) + --csr string, -c string Certificate signing request filename, if an external CSR is to be used. --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 string Define a hook. The hook is executed only when the certificates are effectively created/renewed. --run-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s) + --private-key string Path to private key (in PEM encoding) for the certificate. By default, the private key is generated. --help, -h show help """ @@ -93,10 +92,9 @@ USAGE: lego renew OPTIONS: - --domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times. + --domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times or use comma as a separator. --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] @@ -108,7 +106,6 @@ OPTIONS: --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] @@ -131,11 +128,7 @@ OPTIONS: --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. + --csr string, -c string Certificate signing request filename, if an external CSR is to be used. --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) @@ -143,8 +136,13 @@ OPTIONS: --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 string Define a hook. The hook is executed only when the certificates are effectively created/renewed. --renew-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s) + --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-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 @@ -160,47 +158,28 @@ USAGE: lego revoke OPTIONS: - --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 + --domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times or use comma as a separator. + --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] + --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 + --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] + --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]] diff --git a/e2e/challenges_test.go b/e2e/challenges_test.go index 51a27a937..e0e76234e 100644 --- a/e2e/challenges_test.go +++ b/e2e/challenges_test.go @@ -174,9 +174,7 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) { "--accept-tos", "-s", "https://localhost:14000/dir", "-d", testDomain2, - "--tls", - "--tls.port", ":5001", - "revoke") + ) if err != nil { t.Fatal(err) } @@ -204,8 +202,6 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { "--accept-tos", "-s", "https://localhost:14000/dir", "-d", testDomain4, - "--tls", - "--tls.port", ":5001", ) if err != nil { t.Fatal(err)