2019-03-11 16:56:48 +00:00
package cmd
2018-12-06 21:50:17 +00:00
import (
"crypto"
"crypto/x509"
"time"
2019-07-30 19:19:32 +00:00
"github.com/go-acme/lego/v3/certcrypto"
"github.com/go-acme/lego/v3/certificate"
"github.com/go-acme/lego/v3/lego"
"github.com/go-acme/lego/v3/log"
2018-12-06 21:50:17 +00:00
"github.com/urfave/cli"
)
2020-04-11 12:57:06 +00:00
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
)
2018-12-06 21:50:17 +00:00
func createRenew ( ) cli . Command {
return cli . Command {
Name : "renew" ,
Usage : "Renew a certificate" ,
Action : renew ,
Before : func ( ctx * cli . Context ) error {
// we require either domains or csr, but not both
hasDomains := len ( ctx . GlobalStringSlice ( "domains" ) ) > 0
hasCsr := len ( ctx . GlobalString ( "csr" ) ) > 0
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 {
cli . IntFlag {
Name : "days" ,
2019-02-08 01:43:05 +00:00
Value : 30 ,
2018-12-06 21:50:17 +00:00
Usage : "The number of days left on a certificate to renew it." ,
} ,
cli . BoolFlag {
Name : "reuse-key" ,
Usage : "Used to indicate you want to reuse your current private key for the new certificate." ,
} ,
cli . BoolFlag {
Name : "no-bundle" ,
Usage : "Do not create a certificate bundle by adding the issuers certificate to the new certificate." ,
} ,
cli . BoolFlag {
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." ,
} ,
2019-04-02 16:38:23 +00:00
cli . StringFlag {
Name : "renew-hook" ,
Usage : "Define a hook. The hook is executed only when the certificates are effectively renewed." ,
} ,
2020-09-02 00:22:53 +00:00
cli . StringFlag {
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." ,
} ,
2018-12-06 21:50:17 +00:00
} ,
}
}
func renew ( ctx * cli . Context ) error {
account , client := setup ( ctx , NewAccountsStorage ( ctx ) )
2019-03-11 15:54:35 +00:00
setupChallenges ( ctx , client )
2018-12-06 21:50:17 +00: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 12:57:06 +00:00
meta := map [ string ] string { renewEnvAccountEmail : account . Email }
2018-12-06 21:50:17 +00:00
// CSR
if ctx . GlobalIsSet ( "csr" ) {
2020-04-11 12:57:06 +00:00
return renewForCSR ( ctx , client , certsStorage , bundle , meta )
2018-12-06 21:50:17 +00:00
}
// Domains
2020-04-11 12:57:06 +00:00
return renewForDomains ( ctx , client , certsStorage , bundle , meta )
2018-12-06 21:50:17 +00:00
}
2020-04-11 12:57:06 +00:00
func renewForDomains ( ctx * cli . Context , client * lego . Client , certsStorage * CertificatesStorage , bundle bool , meta map [ string ] string ) error {
2018-12-06 21:50:17 +00:00
domains := ctx . GlobalStringSlice ( "domains" )
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 {
2020-09-02 00:22:53 +00:00
Domains : merge ( certDomains , domains ) ,
Bundle : bundle ,
PrivateKey : privateKey ,
MustStaple : ctx . Bool ( "must-staple" ) ,
PreferredChain : ctx . String ( "preferred-chain" ) ,
2018-12-06 21:50:17 +00:00
}
certRes , err := client . Certificate . Obtain ( request )
if err != nil {
log . Fatal ( err )
}
certsStorage . SaveResource ( certRes )
2020-04-11 12:57:06 +00:00
meta [ renewEnvCertDomain ] = domain
2020-05-11 08:29:39 +00:00
meta [ renewEnvCertPath ] = certsStorage . GetFileName ( domain , ".crt" )
meta [ renewEnvCertKeyPath ] = certsStorage . GetFileName ( domain , ".key" )
2020-04-11 12:57:06 +00:00
2020-05-14 21:44:08 +00:00
return launchHook ( ctx . String ( "renew-hook" ) , meta )
2018-12-06 21:50:17 +00:00
}
2020-04-11 12:57:06 +00:00
func renewForCSR ( ctx * cli . Context , client * lego . Client , certsStorage * CertificatesStorage , bundle bool , meta map [ string ] string ) error {
2018-12-06 21:50:17 +00:00
csr , err := readCSRFile ( ctx . GlobalString ( "csr" ) )
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 00:22:53 +00:00
certRes , err := client . Certificate . ObtainForCSR ( * csr , bundle , ctx . String ( "preferred-chain" ) )
2018-12-06 21:50:17 +00:00
if err != nil {
log . Fatal ( err )
}
certsStorage . SaveResource ( certRes )
2020-04-11 12:57:06 +00:00
meta [ renewEnvCertDomain ] = domain
2020-05-11 08:29:39 +00:00
meta [ renewEnvCertPath ] = certsStorage . GetFileName ( domain , ".crt" )
meta [ renewEnvCertKeyPath ] = certsStorage . GetFileName ( domain , ".key" )
2020-04-11 12:57:06 +00:00
2020-05-14 21:44:08 +00:00
return launchHook ( ctx . String ( "renew-hook" ) , meta )
2018-12-06 21:50:17 +00: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 16:08:48 +00: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 21:50:17 +00:00
return false
}
}
return true
}
2020-07-09 23:48:18 +00:00
func merge ( prevDomains , nextDomains [ ] string ) [ ] string {
2018-12-06 21:50:17 +00: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
}