2019-03-11 18:56:48 +02:00
package cmd
2018-12-06 23:50:17 +02:00
import (
"crypto"
"crypto/x509"
"time"
2020-09-02 03:20:01 +02:00
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/log"
2022-02-13 13:28:51 +02:00
"github.com/urfave/cli/v2"
2018-12-06 23:50:17 +02:00
)
2020-04-11 14:57:06 +02:00
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
2022-03-21 10:36:28 +02:00
renewEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
renewEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
2020-04-11 14:57:06 +02:00
)
2022-02-13 13:28:51 +02:00
func createRenew ( ) * cli . Command {
return & cli . Command {
2018-12-06 23:50:17 +02:00
Name : "renew" ,
Usage : "Renew a certificate" ,
Action : renew ,
Before : func ( ctx * cli . Context ) error {
// we require either domains or csr, but not both
2022-02-13 13:28:51 +02:00
hasDomains := len ( ctx . StringSlice ( "domains" ) ) > 0
hasCsr := len ( ctx . String ( "csr" ) ) > 0
2018-12-06 23:50:17 +02:00
if hasDomains && hasCsr {
log . Fatal ( "Please specify either --domains/-d or --csr/-c, but not both" )
}
if ! hasDomains && ! hasCsr {
log . Fatal ( "Please specify --domains/-d (or --csr/-c if you already have a CSR)" )
}
return nil
} ,
Flags : [ ] cli . Flag {
2022-02-13 13:28:51 +02:00
& cli . IntFlag {
2018-12-06 23:50:17 +02:00
Name : "days" ,
2019-02-08 03:43:05 +02:00
Value : 30 ,
2018-12-06 23:50:17 +02:00
Usage : "The number of days left on a certificate to renew it." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . BoolFlag {
2018-12-06 23:50:17 +02:00
Name : "reuse-key" ,
Usage : "Used to indicate you want to reuse your current private key for the new certificate." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . BoolFlag {
2018-12-06 23:50:17 +02:00
Name : "no-bundle" ,
Usage : "Do not create a certificate bundle by adding the issuers certificate to the new certificate." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . BoolFlag {
2018-12-06 23:50:17 +02:00
Name : "must-staple" ,
Usage : "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . StringFlag {
2019-04-02 18:38:23 +02:00
Name : "renew-hook" ,
Usage : "Define a hook. The hook is executed only when the certificates are effectively renewed." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . StringFlag {
2020-09-02 02:22:53 +02:00
Name : "preferred-chain" ,
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." ,
} ,
2022-02-13 13:28:51 +02:00
& cli . StringFlag {
2021-09-08 23:14:21 +02:00
Name : "always-deactivate-authorizations" ,
Usage : "Force the authorizations to be relinquished even if the certificate request was successful." ,
} ,
2018-12-06 23:50:17 +02:00
} ,
}
}
func renew ( ctx * cli . Context ) error {
account , client := setup ( ctx , NewAccountsStorage ( ctx ) )
2019-03-11 17:54:35 +02:00
setupChallenges ( ctx , client )
2018-12-06 23:50:17 +02:00
if account . Registration == nil {
log . Fatalf ( "Account %s is not registered. Use 'run' to register a new account.\n" , account . Email )
}
certsStorage := NewCertificatesStorage ( ctx )
bundle := ! ctx . Bool ( "no-bundle" )
2020-04-11 14:57:06 +02:00
meta := map [ string ] string { renewEnvAccountEmail : account . Email }
2018-12-06 23:50:17 +02:00
// CSR
2022-02-13 13:28:51 +02:00
if ctx . IsSet ( "csr" ) {
2020-04-11 14:57:06 +02:00
return renewForCSR ( ctx , client , certsStorage , bundle , meta )
2018-12-06 23:50:17 +02:00
}
// Domains
2020-04-11 14:57:06 +02:00
return renewForDomains ( ctx , client , certsStorage , bundle , meta )
2018-12-06 23:50:17 +02:00
}
2020-04-11 14:57:06 +02:00
func renewForDomains ( ctx * cli . Context , client * lego . Client , certsStorage * CertificatesStorage , bundle bool , meta map [ string ] string ) error {
2022-02-13 13:28:51 +02:00
domains := ctx . StringSlice ( "domains" )
2018-12-06 23:50:17 +02:00
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 , ".crt" )
if err != nil {
log . Fatalf ( "Error while loading the certificate for domain %s\n\t%v" , domain , err )
}
cert := certificates [ 0 ]
if ! needRenewal ( cert , domain , ctx . Int ( "days" ) ) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert . NotAfter . Sub ( time . Now ( ) . UTC ( ) )
log . Infof ( "[%s] acme: Trying renewal with %d hours remaining" , domain , int ( timeLeft . Hours ( ) ) )
certDomains := certcrypto . ExtractDomains ( cert )
var privateKey crypto . PrivateKey
if ctx . Bool ( "reuse-key" ) {
keyBytes , errR := certsStorage . ReadFile ( domain , ".key" )
if errR != nil {
log . Fatalf ( "Error while loading the private key for domain %s\n\t%v" , domain , errR )
}
privateKey , errR = certcrypto . ParsePEMPrivateKey ( keyBytes )
if errR != nil {
return errR
}
}
request := certificate . ObtainRequest {
2021-09-08 23:14:21 +02:00
Domains : merge ( certDomains , domains ) ,
Bundle : bundle ,
PrivateKey : privateKey ,
MustStaple : ctx . Bool ( "must-staple" ) ,
PreferredChain : ctx . String ( "preferred-chain" ) ,
AlwaysDeactivateAuthorizations : ctx . Bool ( "always-deactivate-authorizations" ) ,
2018-12-06 23:50:17 +02:00
}
certRes , err := client . Certificate . Obtain ( request )
if err != nil {
log . Fatal ( err )
}
certsStorage . SaveResource ( certRes )
2020-04-11 14:57:06 +02:00
meta [ renewEnvCertDomain ] = domain
2020-05-11 10:29:39 +02:00
meta [ renewEnvCertPath ] = certsStorage . GetFileName ( domain , ".crt" )
meta [ renewEnvCertKeyPath ] = certsStorage . GetFileName ( domain , ".key" )
2022-03-21 10:36:28 +02:00
meta [ renewEnvCertPEMPath ] = certsStorage . GetFileName ( domain , ".pem" )
meta [ renewEnvCertPFXPath ] = certsStorage . GetFileName ( domain , ".pfx" )
2020-04-11 14:57:06 +02:00
2020-05-14 23:44:08 +02:00
return launchHook ( ctx . String ( "renew-hook" ) , meta )
2018-12-06 23:50:17 +02:00
}
2020-04-11 14:57:06 +02:00
func renewForCSR ( ctx * cli . Context , client * lego . Client , certsStorage * CertificatesStorage , bundle bool , meta map [ string ] string ) error {
2022-02-13 13:28:51 +02:00
csr , err := readCSRFile ( ctx . String ( "csr" ) )
2018-12-06 23:50:17 +02:00
if err != nil {
log . Fatal ( err )
}
domain := csr . Subject . CommonName
// 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 , ".crt" )
if err != nil {
log . Fatalf ( "Error while loading the certificate for domain %s\n\t%v" , domain , err )
}
cert := certificates [ 0 ]
if ! needRenewal ( cert , domain , ctx . Int ( "days" ) ) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert . NotAfter . Sub ( time . Now ( ) . UTC ( ) )
log . Infof ( "[%s] acme: Trying renewal with %d hours remaining" , domain , int ( timeLeft . Hours ( ) ) )
2020-09-02 02:31:53 +02:00
certRes , err := client . Certificate . ObtainForCSR ( certificate . ObtainForCSRRequest {
2021-09-08 23:14:21 +02:00
CSR : csr ,
Bundle : bundle ,
PreferredChain : ctx . String ( "preferred-chain" ) ,
AlwaysDeactivateAuthorizations : ctx . Bool ( "always-deactivate-authorizations" ) ,
2020-09-02 02:31:53 +02:00
} )
2018-12-06 23:50:17 +02:00
if err != nil {
log . Fatal ( err )
}
certsStorage . SaveResource ( certRes )
2020-04-11 14:57:06 +02:00
meta [ renewEnvCertDomain ] = domain
2020-05-11 10:29:39 +02:00
meta [ renewEnvCertPath ] = certsStorage . GetFileName ( domain , ".crt" )
meta [ renewEnvCertKeyPath ] = certsStorage . GetFileName ( domain , ".key" )
2020-04-11 14:57:06 +02:00
2020-05-14 23:44:08 +02:00
return launchHook ( ctx . String ( "renew-hook" ) , meta )
2018-12-06 23:50:17 +02:00
}
func needRenewal ( x509Cert * x509 . Certificate , domain string , days int ) bool {
if x509Cert . IsCA {
log . Fatalf ( "[%s] Certificate bundle starts with a CA certificate" , domain )
}
if days >= 0 {
2019-03-11 18:08:48 +02:00
notAfter := int ( time . Until ( x509Cert . NotAfter ) . Hours ( ) / 24.0 )
if notAfter > days {
log . Printf ( "[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal." ,
domain , notAfter , days )
2018-12-06 23:50:17 +02:00
return false
}
}
return true
}
2020-07-10 01:48:18 +02:00
func merge ( prevDomains , nextDomains [ ] string ) [ ] string {
2018-12-06 23:50:17 +02:00
for _ , next := range nextDomains {
var found bool
for _ , prev := range prevDomains {
if prev == next {
found = true
break
}
}
if ! found {
prevDomains = append ( prevDomains , next )
}
}
return prevDomains
}