From dbc68eb9de750cf60396988d22f4247003276136 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 30 Mar 2021 15:17:04 -0400 Subject: [PATCH] Detect an expired certificate and regenerate it --- cert/tls.go | 4 + certprovider/zerossl.go | 25 +++-- lnd.go | 237 ++++++++++++++++++++++++++++------------ 3 files changed, 191 insertions(+), 75 deletions(-) diff --git a/cert/tls.go b/cert/tls.go index a28fc5858b..ce9685b579 100644 --- a/cert/tls.go +++ b/cert/tls.go @@ -142,3 +142,7 @@ func (tlsr *TlsReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) ( return tlsr.cert, nil } } + +func (tlsr *TlsReloader) GetCert() *tls.Certificate { + return tlsr.cert +} diff --git a/certprovider/zerossl.go b/certprovider/zerossl.go index d785507393..2ee7ec0d6c 100644 --- a/certprovider/zerossl.go +++ b/certprovider/zerossl.go @@ -66,7 +66,18 @@ type ZeroSSLCertRevoke struct { Success int `json:"success"` } -func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { +type CertProvider interface { + GenerateCsr([]byte, string) (bytes.Buffer, error) + RequestCert(bytes.Buffer, string) (ZeroSSLExternalCert, error) + ValidateCert(ZeroSSLExternalCert) error + GetCert(string) (ZeroSSLExternalCert, error) + DownloadCert(ZeroSSLExternalCert) (string, string, error) + RevokeCert(string) error +} + +type ZeroSSL struct{} + +func (ZeroSSL) GenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { block, _ := pem.Decode(keyBytes) x509Encoded := block.Bytes privKey, err := x509.ParsePKCS1PrivateKey(x509Encoded) @@ -90,7 +101,7 @@ func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, return csrBuffer, nil } -func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { +func (ZeroSSL) RequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return certificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -132,7 +143,7 @@ func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExt return certificate, nil } -func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { +func (ZeroSSL) ValidateCert(certificate ZeroSSLExternalCert) error { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -161,14 +172,14 @@ func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { return nil } -func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExternalCert, err error) { +func (ZeroSSL) GetCert(certId string) (newCertificate ZeroSSLExternalCert, err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return newCertificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") } apiUrl := fmt.Sprintf( "%s/certificates/%s?access_key=%s", - zeroSSLBaseUrl, certificate.Id, apiKey, + zeroSSLBaseUrl, certId, apiKey, ) client := &http.Client{} request, err := http.NewRequest("GET", apiUrl, nil) @@ -201,7 +212,7 @@ func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExte return newCertificate, nil } -func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { +func (ZeroSSL) DownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return "", "", fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -242,7 +253,7 @@ func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error return certResponse.Certificate, certResponse.CaBundle, nil } -func ZeroSSLRevokeCert(certificateId string) (err error) { +func (ZeroSSL) RevokeCert(certificateId string) (err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") diff --git a/lnd.go b/lnd.go index 5151bbeb28..2de2b2f40c 100644 --- a/lnd.go +++ b/lnd.go @@ -25,6 +25,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" + "github.com/go-co-op/gocron" proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" @@ -272,6 +273,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { var restListen func(net.Addr) (net.Listener, error) var tlsReloader *cert.TlsReloader var certId string + externalCertMaker := externalCert{} // The real KeyRing isn't available until after the wallet is unlocked, // but we need one now. Because we aren't encrypting anything here it can @@ -295,7 +297,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { restListen, cleanUp, tlsReloader, - err = getTLSConfig(cfg, emptyKeyRing) + certId, + err = getTLSConfig(cfg, emptyKeyRing, externalCertMaker) } if err != nil { err := fmt.Errorf("unable to load TLS credentials: %v", err) @@ -766,43 +769,40 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { // The wallet is unlocked at this point so we can use the real KeyRing if cfg.TLSEncryptKey { tmpCertPath := cfg.TLSCertPath + ".tmp" - tmpExternalCertPath := fmt.Sprintf("%s/%s/tls.cert.tmp", cfg.LndDir, cfg.ExternalSSLProvider) - err = os.Remove(tmpCertPath) - if err != nil { - ltndLog.Warn("unable to delete temp cert at %v", tmpCertPath) - } + zerossl := certprovider.ZeroSSL{} - err = os.Remove(tmpExternalCertPath) + err := DeleteAndRegenerateCert(tmpCertPath, zerossl, cfg, activeChainControl, certId, tlsReloader, externalCertMaker) if err != nil { - ltndLog.Warn("unable to delete temp external cert at %v", tmpExternalCertPath) - } - _, _, _, _, _, err = getTLSConfig(cfg, emptyKeyRing) - if err != nil { - err := fmt.Errorf("unable to load TLS credentials: %v", err) + err := fmt.Errorf("unable to delete and regenerate certificate: %v", err) ltndLog.Error(err) - return err - } - certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) - if err != nil { - return err - } - reader := bytes.NewReader(keyBytes) - keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, activeChainControl.KeyRing) - if err != nil { - return err - } - err = certprovider.ZeroSSLRevokeCert(certId) - if err != nil { - ltndLog.Error("Failed to revoke temporary certifiate:") - ltndLog.Error(err) - } - // Switch the server's TLS certificate to the persisntent one - err = tlsReloader.AttemptReload(certBytes, keyBytes) - if err != nil { - return err } } + // If we're using ZeroSSL, we'll spin up a goroutine to check when the certificate expires. + // If it's expiring in three days or less, we'll generate a new certificate using ZeroSSL. + if cfg.ExternalSSLProvider == "zerossl" { + zerossl := certprovider.ZeroSSL{} + + s := gocron.NewScheduler(time.UTC) + + s.Every(1).Day().Do(func() { + expires, err := CheckForExpiredCert(cfg) + if err != nil { + err := fmt.Errorf("failed to check whether certificate is expiring: %v", err) + ltndLog.Error(err) + } + if expires { + err := DeleteAndRegenerateCert(cfg.TLSCertPath, zerossl, cfg, activeChainControl, certId, tlsReloader, externalCert{}) + if err != nil { + err := fmt.Errorf("unable to delete and regenerate certificate: %v", err) + ltndLog.Error(err) + } + } + }) + + s.StartAsync() + } + // Now we have created all dependencies necessary to populate and // start the RPC server. err = rpcServer.addDeps( @@ -910,16 +910,24 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { return nil } -// createExternalCert creates an Externally provisioned SSL Certificate -func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (returnCert tls.Certificate, certId string, err error) { +type externalCertMaker interface { + create(*Config, []byte, string) (tls.Certificate, string, error) +} + +type externalCert struct{} + +// createExternalCert creates an Externally provisioned SSL Certificate. +func (c externalCert) create(cfg *Config, keyBytes []byte, certLocation string) (returnCert tls.Certificate, certId string, err error) { var certServer *http.Server if cfg.ExternalSSLProvider == "zerossl" { - csr, err := certprovider.ZeroSSLGenerateCsr(keyBytes, cfg.ExternalSSLDomain) + zerossl := certprovider.ZeroSSL{} + + csr, err := zerossl.GenerateCsr(keyBytes, cfg.ExternalSSLDomain) if err != nil { return returnCert, certId, err } rpcsLog.Debugf("created csr for %s", cfg.ExternalSSLDomain) - externalCert, err := certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + externalCert, err := zerossl.RequestCert(csr, cfg.ExternalSSLDomain) if err != nil { return returnCert, certId, err } @@ -950,7 +958,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu return } }() - err = certprovider.ZeroSSLValidateCert(externalCert) + err = zerossl.ValidateCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -959,7 +967,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu checkCount := 0 retries := 0 for { - newCert, err := certprovider.ZeroSSLGetCert(externalCert) + newCert, err := zerossl.GetCert(externalCert.Id) if err != nil { certServer.Close() return returnCert, certId, err @@ -970,7 +978,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu rpcsLog.Infof("found certificate in state %s", status) break } else if status == "draft" { - err = certprovider.ZeroSSLValidateCert(externalCert) + err = zerossl.ValidateCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -983,7 +991,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu } if checkCount > 15 { rpcsLog.Warn("Timed out waiting for cert. Requesting a new one.") - externalCert, err = certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + externalCert, err = zerossl.RequestCert(csr, cfg.ExternalSSLDomain) if err != nil { certServer.Close() return returnCert, certId, err @@ -996,7 +1004,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu time.Sleep(2 * time.Second) } certId = externalCert.Id - certificate, caBundle, err := certprovider.ZeroSSLDownloadCert(externalCert) + certificate, caBundle, err := zerossl.DownloadCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -1057,7 +1065,8 @@ func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( var certId string var failedProvision bool if cfg.ExternalSSLProvider != "" { - externalCertData, certId, err = createExternalCert( + externalCertMaker := externalCert{} + externalCertData, certId, err = externalCertMaker.create( cfg, keyBytes, externalSSLCertPath, ) if err != nil { @@ -1126,9 +1135,9 @@ func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( // getTLSConfig returns a TLS configuration for the gRPC server and credentials // and a proxy destination for the REST reverse proxy. The cert and key are // written to disk and the private key can be optionally encrypted. -func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( +func getTLSConfig(cfg *Config, keyRing keychain.KeyRing, externalCertMaker externalCertMaker) ( []grpc.ServerOption, []grpc.DialOption, - func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, string, error) { externalSSLCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) keyType := "ec" @@ -1148,7 +1157,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // If the external ssl provider is supplied and there was a key rotation @@ -1163,7 +1172,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // Do a check to see if the TLS private key is encrypted. If it's encrypted, @@ -1174,7 +1183,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( // --tlsencryptkey we error out. This is because the wallet is not // unlocked yet and we don't have access to the keys yet for decrypt. if !cfg.TLSEncryptKey { - return nil, nil, nil, nil, nil, fmt.Errorf("it appears the TLS key is " + + return nil, nil, nil, nil, nil, "", fmt.Errorf("it appears the TLS key is " + "encrypted but you didn't pass the --tlsencryptkey flag. " + "Please restart lnd with the --tlsencryptkey flag or delete " + "the TLS files for regeneration") @@ -1182,7 +1191,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( reader := bytes.NewReader(keyBytes) keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } else if cfg.TLSEncryptKey { // If the user requests an encrypted key but the key is in plaintext @@ -1191,21 +1200,22 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( var b bytes.Buffer err = lnencrypt.EncryptPayloadToWriter(*keyBuf, &b, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if err = ioutil.WriteFile(cfg.TLSKeyPath, b.Bytes(), 0600); err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } var externalCertData tls.Certificate var failedProvision bool + var certId string if cfg.ExternalSSLProvider != "" { // Ensure we create external TLS certificate if they don't exist. if !fileExists(externalSSLCertPath) { ltndLog.Infof("Requesting external certificate for domain %v", cfg.ExternalSSLDomain) - _, _, err = createExternalCert( + _, certId, err = externalCertMaker.create( cfg, keyBytes, externalSSLCertPath, ) if err != nil { @@ -1216,13 +1226,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if !failedProvision { externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } externalCertData, _, err = cert.LoadCert( externalCertBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } } @@ -1231,7 +1241,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // We check whether the certifcate we have on disk match the IPs and @@ -1245,7 +1255,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSExtraDomains, cfg.TLSDisableAutofill, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1257,18 +1267,18 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( err := os.Remove(cfg.TLSCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } err = os.Remove(cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if cfg.ExternalSSLProvider != "" { err = os.Remove(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1280,22 +1290,23 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } rpcsLog.Infof("Done renewing TLS certificates") // Reload the certificate data. certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if cfg.ExternalSSLProvider != "" { // Ensure we create external TLS certificate if they don't exist. if !fileExists(externalSSLCertPath) { + externalCertMaker := externalCert{} ltndLog.Infof("Requesting external certificate for domain %v", cfg.ExternalSSLDomain) - _, _, err = createExternalCert( + _, _, err = externalCertMaker.create( cfg, keyBytes, externalSSLCertPath, ) if err != nil { @@ -1306,13 +1317,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if !failedProvision { externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } externalCertData, _, err = cert.LoadCert( externalCertBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } } @@ -1324,7 +1335,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( reader := bytes.NewReader(keyBytes) keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1332,13 +1343,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } certList := []tls.Certificate{certData} if cfg.ExternalSSLProvider != "" && !failedProvision { @@ -1349,7 +1360,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // If Let's Encrypt is enabled, instantiate autocert to request/renew @@ -1436,7 +1447,97 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return lncfg.TLSListenOnAddress(addr, tlsCfg) } - return serverOpts, restDialOpts, restListen, cleanUp, tlsr, nil + return serverOpts, restDialOpts, restListen, cleanUp, tlsr, certId, nil +} + +// CheckForExpiredCert finds whether the TLS certificate is expiring soon. +func CheckForExpiredCert(cfg *Config) (bool, error) { + externalCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) + + certBytes, keyBytes, err := cert.GetCertBytesFromPath(externalCertPath, cfg.TLSKeyPath) + if err != nil { + return false, err + } + + _, certData, err := cert.LoadCert( + certBytes, keyBytes, + ) + if err != nil { + return false, err + } + + expiresTime := certData.NotAfter + currTime := time.Now() + timeRemaining := expiresTime.Sub(currTime).Hours() + + // 72 hours == three days + return timeRemaining < 72, nil +} + +// DeleteAndRegenerateCert deletes a certificate, either because it was a temporary certificate that is no longer needed, or it's +// about to expire. Then it regenerates a new one and attempts to reload the certificate. +func DeleteAndRegenerateCert(certPath string, certprovider certprovider.CertProvider, cfg *Config, activeChainControl *chainreg.ChainControl, certId string, tlsReloader *cert.TlsReloader, externalCertMaker externalCertMaker) error { + + if fileExists(certPath) { + err := os.Remove(cfg.TLSCertPath) + if err != nil { + ltndLog.Warn("unable to delete cert at %v", cfg.TLSCertPath) + return err + } + } + + if fileExists(cfg.TLSKeyPath) { + err := os.Remove(cfg.TLSKeyPath) + if err != nil { + ltndLog.Warn("unable to delete cert at %v", cfg.TLSCertPath) + return err + } + } + + externalCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) + + if fileExists(externalCertPath) { + err := os.Remove(externalCertPath) + if err != nil { + ltndLog.Warn("unable to delete temp or expiring cert at %v", externalCertPath) + return err + } + } + + _, _, _, _, _, _, err := getTLSConfig(cfg, activeChainControl.KeyRing, externalCertMaker) + if err != nil { + ltndLog.Error("unable to load TLS credentials: %v", err) + return err + } + + certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) + if err != nil { + return err + } + + if cfg.TLSEncryptKey { + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, activeChainControl.KeyRing) + if err != nil { + ltndLog.Error(err) + return err + } + } + + err = certprovider.RevokeCert(certId) + if err != nil { + ltndLog.Error("Failed to revoke temporary certifiate:") + return err + } + + // Switch the server's TLS certificate to the new or persistent one. + err = tlsReloader.AttemptReload(certBytes, keyBytes) + if err != nil { + ltndLog.Error(err) + return err + } + + return nil } // fileExists reports whether the named file or directory exists.