diff --git a/config.go b/config.go index 39818397c..8a1ecf0f0 100644 --- a/config.go +++ b/config.go @@ -271,6 +271,7 @@ type Config struct { TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed"` TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set"` TLSCertDuration time.Duration `long:"tlscertduration" description:"The duration for which the auto-generated TLS certificate will be valid for"` + TLSEncryptKey bool `long:"tlsencryptkey" description:"Automatically encrypts the TLS private key and generates ephemeral TLS key pairs when the wallet is locked or not initialized"` NoMacaroons bool `long:"no-macaroons" description:"Disable macaroon authentication, can only be used if server is not listening on a public interface."` AdminMacPath string `long:"adminmacaroonpath" description:"Path to write the admin macaroon for lnd's RPC and REST services if it doesn't exist"` @@ -699,7 +700,7 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) { // User did specify an explicit --configfile, so we check that it does // exist under that path to avoid surprises. case configFilePath != DefaultConfigFile: - if !fileExists(configFilePath) { + if !lnrpc.FileExists(configFilePath) { return nil, fmt.Errorf("specified config file does "+ "not exist in %s", configFilePath) } diff --git a/config_builder.go b/config_builder.go index cf227c0e6..0b8e5a49b 100644 --- a/config_builder.go +++ b/config_builder.go @@ -466,9 +466,9 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, // If the user requested a stateless initialization, no macaroon // files should be created. if !walletInitParams.StatelessInit && - !fileExists(d.cfg.AdminMacPath) && - !fileExists(d.cfg.ReadMacPath) && - !fileExists(d.cfg.InvoiceMacPath) { + !lnrpc.FileExists(d.cfg.AdminMacPath) && + !lnrpc.FileExists(d.cfg.ReadMacPath) && + !lnrpc.FileExists(d.cfg.InvoiceMacPath) { // Create macaroon files for lncli to use if they don't // exist. @@ -495,13 +495,13 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, "--new_mac_root_key with --stateless_init to " + "clean up and invalidate old macaroons." - if fileExists(d.cfg.AdminMacPath) { + if lnrpc.FileExists(d.cfg.AdminMacPath) { d.logger.Warnf(msg, "admin", d.cfg.AdminMacPath) } - if fileExists(d.cfg.ReadMacPath) { + if lnrpc.FileExists(d.cfg.ReadMacPath) { d.logger.Warnf(msg, "readonly", d.cfg.ReadMacPath) } - if fileExists(d.cfg.InvoiceMacPath) { + if lnrpc.FileExists(d.cfg.InvoiceMacPath) { d.logger.Warnf(msg, "invoice", d.cfg.InvoiceMacPath) } } diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 9f911f15f..ea11b9bf3 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -242,6 +242,9 @@ in the lnwire package](https://github.com/lightningnetwork/lnd/pull/7303) * [A bug has been fixed where a reorg would cause zero-conf channels to be deleted from the graph.](https://github.com/lightningnetwork/lnd/pull/7292) +* [Add a flag](https://github.com/lightningnetwork/lnd/pull/6527) to allow for + the option to encrypt the tls key. + ## `lncli` * [Add an `insecure` flag to skip tls auth as well as a `metadata` string slice @@ -415,6 +418,7 @@ refactor the itest for code health and maintenance. # Contributors (Alphabetical Order) * Alejandro Pedraza +* Alyssa Hertig * andreihod * Antoni Spaanderman * Carla Kirk-Cohen diff --git a/docs/safety.md b/docs/safety.md index b2dae6cac..ec59722c9 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -89,6 +89,47 @@ directory) is missing on startup, a new self-signed key/certificate pair is generated. Clients connecting to `lnd` then have to use the new certificate to verify they are talking to the correct server. +#### TLS Key Encryption + +By default, LND writes the TLS key to disk in plaintext. If you run in an +untrusted environment you may want to encrypt the TLS key so no one can +snoop on your API traffic. This can be accomplished with the `--tlsencryptkey` +flag in LND. When this is set, LND encrypts the TLS key using the wallet's +seed and writes the encrypted blob to disk. + +Because the key is encrypted to the wallet's seed, that means we can only use +the TLS pair when the wallet is unlocked. This would leave the +`WalletUnlocker` service without TLS. To circumvent this problem, LND uses a +temporary TLS pair for the `WalletUnlocker` service. To avoid writing the +temporary key to disk, it is held in memory until the wallet is unlocked. The +temporary TLS cert is written to disk using the same value as `tlscertpath` +with `.tmp` appended to the end. Once the wallet is unlocked, the temporary +TLS cert is deleted from disk and the TLS key is removed from memory. Then +LND uses the main TLS cert and key after it's decrypted. + +This requires a slight change in behavior when connecting to LND's APIs. +When `--tlsencryptkey` is set on LND, you will need to access the temporary +TLS cert for the initialize, unlock, and change password API calls. You can +do this in `lncli` by simply pointing the `--tlscertpath` flag at the temporary +TLS cert for the `create`, `unlock`, and `changepassword` commands. If you +aren't able to run `lncli` on the host `lnd` is running on, then you'll need +to copy the temporary certificate from the host onto whatever device you're +using. Ignoring TLS certificate verification is considered insecure and not +recommended. + +_Important Considerations:_ + +- Once you set `--tlsencryptkey` when starting LND, you'll always need to use +the flag. If you don't want to encrypt the TLS key anymore you'll have to +delete the TLS cert and key so LND generates a new one in plaintext. + +- The temporary TLS cert still contains the same information as the persistent +certificates. + +- The temporary TLS cert is only valid for 24 hours while the persistent certs +are valid for more than a year. + + ### Macaroons Macaroons are used as the main authentication method in `lnd`. A macaroon is a diff --git a/go.mod b/go.mod index 8428c3c2b..e8d7c8bbe 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/lightninglabs/neutrino v0.14.2 github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 - github.com/lightningnetwork/lnd/cert v1.1.1 + github.com/lightningnetwork/lnd/cert v1.2.0 github.com/lightningnetwork/lnd/clock v1.1.0 github.com/lightningnetwork/lnd/healthcheck v1.2.2 github.com/lightningnetwork/lnd/kvdb v1.3.1 diff --git a/go.sum b/go.sum index 02a68c1fb..2458dcb91 100644 --- a/go.sum +++ b/go.sum @@ -447,6 +447,8 @@ github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1/go.mod h1:7dDx73ApjEZA0kcknI799m2O5kkpfg4/gr7N092ojNo= github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= +github.com/lightningnetwork/lnd/cert v1.2.0 h1:IWfjHNMI5JgQZU5fdvDptF3DkVI38f4jO/s3tYgWFbE= +github.com/lightningnetwork/lnd/cert v1.2.0/go.mod h1:04JhIEodoR6usBN5+XBRtLEEmEHsclLi0tEyxZQNP+w= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= github.com/lightningnetwork/lnd/clock v1.1.0 h1:/yfVAwtPmdx45aQBoXQImeY7sOIEr7IXlImRMBOZ7GQ= github.com/lightningnetwork/lnd/clock v1.1.0/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= diff --git a/lnd.go b/lnd.go index 11c7e539f..8da903e96 100644 --- a/lnd.go +++ b/lnd.go @@ -6,7 +6,6 @@ package lnd import ( "context" - "crypto/tls" "errors" "fmt" "io/ioutil" @@ -23,7 +22,6 @@ import ( proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" - "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" @@ -37,7 +35,6 @@ import ( "github.com/lightningnetwork/lnd/tor" "github.com/lightningnetwork/lnd/walletunlocker" "github.com/lightningnetwork/lnd/watchtower" - "golang.org/x/crypto/acme/autocert" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/encoding/protojson" @@ -216,13 +213,31 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, return mkErr("error initializing DBs: %v", err) } - // Only process macaroons if --no-macaroons isn't set. - serverOpts, restDialOpts, restListen, cleanUp, err := getTLSConfig(cfg) - if err != nil { - return mkErr("unable to load TLS credentials: %v", err) - } + tlsManagerCfg := &TLSManagerCfg{ + TLSCertPath: cfg.TLSCertPath, + TLSKeyPath: cfg.TLSKeyPath, + TLSEncryptKey: cfg.TLSEncryptKey, + TLSExtraIPs: cfg.TLSExtraIPs, + TLSExtraDomains: cfg.TLSExtraDomains, + TLSAutoRefresh: cfg.TLSAutoRefresh, + TLSDisableAutofill: cfg.TLSDisableAutofill, + TLSCertDuration: cfg.TLSCertDuration, - defer cleanUp() + LetsEncryptDir: cfg.LetsEncryptDir, + LetsEncryptDomain: cfg.LetsEncryptDomain, + LetsEncryptListen: cfg.LetsEncryptListen, + + DisableRestTLS: cfg.DisableRestTLS, + } + tlsManager := NewTLSManager(tlsManagerCfg) + serverOpts, restDialOpts, restListen, cleanUp, + err := tlsManager.SetCertificateBeforeUnlock() + if err != nil { + return mkErr("error setting cert before unlock: %v", err) + } + if cleanUp != nil { + defer cleanUp() + } // If we have chosen to start with a dedicated listener for the // rpc server, we set it directly. @@ -513,7 +528,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, server, err := newServer( cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc, activeChainControl.Cfg.WalletUnlockParams.ChansToRestore, - multiAcceptor, torController, + multiAcceptor, torController, tlsManager, ) if err != nil { return mkErr("unable to create server: %v", err) @@ -539,6 +554,12 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, } defer atplManager.Stop() + err = tlsManager.LoadPermanentCertificate(activeChainControl.KeyRing) + if err != nil { + return mkErr("unable to load permanent TLS certificate: %v", + err) + } + // Now we have created all dependencies necessary to populate and // start the RPC server. err = rpcServer.addDeps( @@ -630,188 +651,6 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, return nil } -// getTLSConfig returns a TLS configuration for the gRPC server and credentials -// and a proxy destination for the REST reverse proxy. -func getTLSConfig(cfg *Config) ([]grpc.ServerOption, []grpc.DialOption, - func(net.Addr) (net.Listener, error), func(), error) { - - // Ensure we create TLS key and certificate if they don't exist. - if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { - rpcsLog.Infof("Generating TLS certificates...") - err := cert.GenCertPair( - "lnd autogenerated cert", cfg.TLSCertPath, - cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, - cfg.TLSDisableAutofill, cfg.TLSCertDuration, - ) - if err != nil { - return nil, nil, nil, nil, err - } - rpcsLog.Infof("Done generating TLS certificates") - } - - certData, parsedCert, err := cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, - ) - if err != nil { - return nil, nil, nil, nil, err - } - - // We check whether the certificate we have on disk match the IPs and - // domains specified by the config. If the extra IPs or domains have - // changed from when the certificate was created, we will refresh the - // certificate if auto refresh is active. - refresh := false - if cfg.TLSAutoRefresh { - refresh, err = cert.IsOutdated( - parsedCert, cfg.TLSExtraIPs, - cfg.TLSExtraDomains, cfg.TLSDisableAutofill, - ) - if err != nil { - return nil, nil, nil, nil, err - } - } - - // If the certificate expired or it was outdated, delete it and the TLS - // key and generate a new pair. - if time.Now().After(parsedCert.NotAfter) || refresh { - ltndLog.Info("TLS certificate is expired or outdated, " + - "generating a new one") - - err := os.Remove(cfg.TLSCertPath) - if err != nil { - return nil, nil, nil, nil, err - } - - err = os.Remove(cfg.TLSKeyPath) - if err != nil { - return nil, nil, nil, nil, err - } - - rpcsLog.Infof("Renewing TLS certificates...") - err = cert.GenCertPair( - "lnd autogenerated cert", cfg.TLSCertPath, - cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, - cfg.TLSDisableAutofill, cfg.TLSCertDuration, - ) - if err != nil { - return nil, nil, nil, nil, err - } - rpcsLog.Infof("Done renewing TLS certificates") - - // Reload the certificate data. - certData, _, err = cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, - ) - if err != nil { - return nil, nil, nil, nil, err - } - } - - tlsCfg := cert.TLSConfFromCert(certData) - - restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") - if err != nil { - return nil, nil, nil, nil, err - } - - // If Let's Encrypt is enabled, instantiate autocert to request/renew - // the certificates. - cleanUp := func() {} - if cfg.LetsEncryptDomain != "" { - ltndLog.Infof("Using Let's Encrypt certificate for domain %v", - cfg.LetsEncryptDomain) - - manager := autocert.Manager{ - Cache: autocert.DirCache(cfg.LetsEncryptDir), - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(cfg.LetsEncryptDomain), - } - - srv := &http.Server{ - Addr: cfg.LetsEncryptListen, - Handler: manager.HTTPHandler(nil), - } - shutdownCompleted := make(chan struct{}) - cleanUp = func() { - err := srv.Shutdown(context.Background()) - if err != nil { - ltndLog.Errorf("Autocert listener shutdown "+ - " error: %v", err) - - return - } - <-shutdownCompleted - ltndLog.Infof("Autocert challenge listener stopped") - } - - go func() { - ltndLog.Infof("Autocert challenge listener started "+ - "at %v", cfg.LetsEncryptListen) - - err := srv.ListenAndServe() - if err != http.ErrServerClosed { - ltndLog.Errorf("autocert http: %v", err) - } - close(shutdownCompleted) - }() - - getCertificate := func(h *tls.ClientHelloInfo) ( - *tls.Certificate, error) { - - lecert, err := manager.GetCertificate(h) - if err != nil { - ltndLog.Errorf("GetCertificate: %v", err) - return &certData, nil - } - - return lecert, err - } - - // The self-signed tls.cert remains available as fallback. - tlsCfg.GetCertificate = getCertificate - } - - serverCreds := credentials.NewTLS(tlsCfg) - serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} - - // For our REST dial options, we'll still use TLS, but also increase - // the max message size that we'll decode to allow clients to hit - // endpoints which return more data such as the DescribeGraph call. - // We set this to 200MiB atm. Should be the same value as maxMsgRecvSize - // in cmd/lncli/main.go. - restDialOpts := []grpc.DialOption{ - grpc.WithTransportCredentials(restCreds), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize), - ), - } - - // Return a function closure that can be used to listen on a given - // address with the current TLS config. - restListen := func(addr net.Addr) (net.Listener, error) { - // For restListen we will call ListenOnAddress if TLS is - // disabled. - if cfg.DisableRestTLS { - return lncfg.ListenOnAddress(addr) - } - - return lncfg.TLSListenOnAddress(addr, tlsCfg) - } - - return serverOpts, restDialOpts, restListen, cleanUp, nil -} - -// fileExists reports whether the named file or directory exists. -// This function is taken from https://github.com/btcsuite/btcd -func fileExists(name string) bool { - if _, err := os.Stat(name); err != nil { - if os.IsNotExist(err) { - return false - } - } - return true -} - // bakeMacaroon creates a new macaroon with newest version and the given // permissions then returns it binary serialized. func bakeMacaroon(ctx context.Context, svc *macaroons.Service, diff --git a/sample-lnd.conf b/sample-lnd.conf index 4996c21d2..d5f5de65f 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -61,6 +61,9 @@ ; use first --tlsextradomain as Common Name instead, if set. ; tlsdisableautofill=true +; If set, the TLS private key will be encrypted to the node's seed. +; tlsencryptkey=true + ; A list of domains for lnd to periodically resolve, and advertise the resolved ; IPs for the backing node. This is useful for users that only have a dynamic IP, ; or want to expose the node at a domain. diff --git a/server.go b/server.go index 6a6183c1b..8a3d6fedc 100644 --- a/server.go +++ b/server.go @@ -27,7 +27,6 @@ import ( "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/brontide" - "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/chanbackup" @@ -294,6 +293,8 @@ type server struct { readPool *pool.Read + tlsManager *TLSManager + // featureMgr dispatches feature vectors for various contexts within the // daemon. featureMgr *feature.Manager @@ -473,7 +474,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, nodeKeyDesc *keychain.KeyDescriptor, chansToRestore walletunlocker.ChannelsToRecover, chanPredicate chanacceptor.ChannelAcceptor, - torController *tor.Controller) (*server, error) { + torController *tor.Controller, tlsManager *TLSManager) (*server, + error) { var ( err error @@ -600,6 +602,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, customMessageServer: subscribe.NewServer(), + tlsManager: tlsManager, + featureMgr: featureMgr, quit: make(chan struct{}), } @@ -1640,18 +1644,15 @@ func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl) { tlsHealthCheck := healthcheck.NewObservation( "tls", func() error { - _, parsedCert, err := cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, + expired, expTime, err := s.tlsManager.IsCertExpired( + s.cc.KeyRing, ) if err != nil { return err } - - // If the current time is passed the certificate's - // expiry time, then it is considered expired - if time.Now().After(parsedCert.NotAfter) { + if expired { return fmt.Errorf("TLS certificate is "+ - "expired as of %v", parsedCert.NotAfter) + "expired as of %v", expTime) } // If the certificate is not outdated, no error needs diff --git a/server_test.go b/server_test.go index 6c570e332..a60d0d6a1 100644 --- a/server_test.go +++ b/server_test.go @@ -4,151 +4,11 @@ package lnd import ( - "bytes" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "io/ioutil" - "math/big" - "net" "testing" - "time" "github.com/lightningnetwork/lnd/lncfg" - "github.com/stretchr/testify/require" ) -// TestTLSAutoRegeneration creates an expired TLS certificate, to test that a -// new TLS certificate pair is regenerated when the old pair expires. This is -// necessary because the pair expires after a little over a year. -func TestTLSAutoRegeneration(t *testing.T) { - tempDirPath := t.TempDir() - - certPath := tempDirPath + "/tls.cert" - keyPath := tempDirPath + "/tls.key" - - certDerBytes, keyBytes := genExpiredCertPair(t, tempDirPath) - expiredCert, err := x509.ParseCertificate(certDerBytes) - require.NoError(t, err, "failed to parse certificate") - - certBuf := bytes.Buffer{} - err = pem.Encode( - &certBuf, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certDerBytes, - }, - ) - require.NoError(t, err, "failed to encode certificate") - - keyBuf := bytes.Buffer{} - err = pem.Encode( - &keyBuf, &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: keyBytes, - }, - ) - require.NoError(t, err, "failed to encode private key") - - // Write cert and key files. - err = ioutil.WriteFile(tempDirPath+"/tls.cert", certBuf.Bytes(), 0644) - require.NoError(t, err, "failed to write cert file") - err = ioutil.WriteFile(tempDirPath+"/tls.key", keyBuf.Bytes(), 0600) - require.NoError(t, err, "failed to write key file") - - rpcListener := net.IPAddr{IP: net.ParseIP("127.0.0.1"), Zone: ""} - rpcListeners := make([]net.Addr, 0) - rpcListeners = append(rpcListeners, &rpcListener) - - // Now let's run getTLSConfig. If it works properly, it should delete - // the cert and create a new one. - cfg := &Config{ - TLSCertPath: certPath, - TLSKeyPath: keyPath, - TLSCertDuration: 42 * time.Hour, - RPCListeners: rpcListeners, - } - _, _, _, cleanUp, err := getTLSConfig(cfg) - if err != nil { - t.Fatalf("couldn't retrieve TLS config") - } - t.Cleanup(cleanUp) - - // Grab the certificate to test that getTLSConfig did its job correctly - // and generated a new cert. - newCertData, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - t.Fatalf("couldn't grab new certificate") - } - - newCert, err := x509.ParseCertificate(newCertData.Certificate[0]) - if err != nil { - t.Fatalf("couldn't parse new certificate") - } - - // Check that the expired certificate was successfully deleted and - // replaced with a new one. - if !newCert.NotAfter.After(expiredCert.NotAfter) { - t.Fatalf("New certificate expiration is too old") - } -} - -// genExpiredCertPair generates an expired key/cert pair to test that expired -// certificates are being regenerated correctly. -func genExpiredCertPair(t *testing.T, certDirPath string) ([]byte, []byte) { - // Max serial number. - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - - // Generate a serial number that's below the serialNumberLimit. - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - require.NoError(t, err, "failed to generate serial number") - - host := "lightning" - - // Create a simple ip address for the fake certificate. - ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} - - dnsNames := []string{host, "unix", "unixpacket"} - - // Construct the certificate template. - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"lnd autogenerated cert"}, - CommonName: host, - }, - NotBefore: time.Now().Add(-time.Hour * 24), - NotAfter: time.Now(), - - KeyUsage: x509.KeyUsageKeyEncipherment | - x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - IsCA: true, // so can sign self. - BasicConstraintsValid: true, - - DNSNames: dnsNames, - IPAddresses: ipAddresses, - } - - // Generate a private key for the certificate. - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatalf("failed to generate a private key") - } - - certDerBytes, err := x509.CreateCertificate( - rand.Reader, &template, &template, &priv.PublicKey, priv, - ) - require.NoError(t, err, "failed to create certificate") - - keyBytes, err := x509.MarshalECPrivateKey(priv) - require.NoError(t, err, "unable to encode privkey") - - return certDerBytes, keyBytes -} - // TestShouldPeerBootstrap tests that we properly skip network bootstrap for // the developer networks, and also if bootstrapping is explicitly disabled. func TestShouldPeerBootstrap(t *testing.T) { diff --git a/tls_manager.go b/tls_manager.go new file mode 100644 index 000000000..2bd174ca1 --- /dev/null +++ b/tls_manager.go @@ -0,0 +1,639 @@ +package lnd + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "time" + + "github.com/lightningnetwork/lnd/cert" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lnrpc" + "golang.org/x/crypto/acme/autocert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const ( + // modifyFilePermissons is the file permission used for writing + // encrypted tls files. + modifyFilePermissions = 0600 + + // validityHours is the number of hours the ephemeral tls certificate + // will be valid, if encrypting tls certificates is turned on. + validityHours = 24 +) + +var ( + // privateKeyPrefix is the prefix to a plaintext TLS key. + privateKeyPrefix = []byte("-----BEGIN EC PRIVATE KEY-----") + + // letsEncryptTimeout sets a timeout for the Lets Encrypt server. + letsEncryptTimeout = 5 * time.Second +) + +// TLSManagerCfg houses a set of values and methods that is passed to the +// TLSManager for it to properly manage LND's TLS options. +type TLSManagerCfg struct { + TLSCertPath string + TLSKeyPath string + TLSEncryptKey bool + TLSExtraIPs []string + TLSExtraDomains []string + TLSAutoRefresh bool + TLSDisableAutofill bool + TLSCertDuration time.Duration + + LetsEncryptDir string + LetsEncryptDomain string + LetsEncryptListen string + + DisableRestTLS bool +} + +// TLSManager generates/renews a TLS cert/key pair when needed. When required, +// it encrypts the TLS key. It also returns the certificate configuration +// options needed for gRPC and REST. +type TLSManager struct { + cfg *TLSManagerCfg + + // tlsReloader is able to reload the certificate with the + // GetCertificate function. In getConfig, tlsCfg.GetCertificate is + // pointed towards t.tlsReloader.GetCertificateFunc(). When + // TLSReloader's AttemptReload is called, the cert that tlsReloader + // holds is changed, in turn changing the cert data + // tlsCfg.GetCertificate will return. + tlsReloader *cert.TLSReloader + + // These options are only used if we're currently using an ephemeral + // TLS certificate, used when we're encrypting the TLS key. + ephemeralKey []byte + ephemeralCert []byte + ephemeralCertPath string +} + +// NewTLSManager returns a reference to a new TLSManager. +func NewTLSManager(cfg *TLSManagerCfg) *TLSManager { + return &TLSManager{ + cfg: cfg, + } +} + +// getConfig returns a TLS configuration for the gRPC server and credentials +// and a proxy destination for the REST reverse proxy. +func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption, + func(net.Addr) (net.Listener, error), error) { + + var ( + keyBytes, certBytes []byte + err error + ) + if t.ephemeralKey != nil { + keyBytes = t.ephemeralKey + certBytes = t.ephemeralCert + } else { + certBytes, keyBytes, err = cert.GetCertBytesFromPath( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + if err != nil { + return nil, nil, nil, err + } + } + + certData, _, err := cert.LoadCertFromBytes(certBytes, keyBytes) + if err != nil { + return nil, nil, nil, err + } + + if t.tlsReloader == nil { + tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) + if err != nil { + return nil, nil, nil, err + } + t.tlsReloader = tlsr + } + + tlsCfg := cert.TLSConfFromCert(certData) + tlsCfg.GetCertificate = t.tlsReloader.GetCertificateFunc() + + // If we're using the ephemeral certificate, we need to use the + // ephemeral cert path. + certPath := t.cfg.TLSCertPath + if t.ephemeralCertPath != "" { + certPath = t.ephemeralCertPath + } + + // Now that we know that we have a certificate, let's generate the + // required config options. + restCreds, err := credentials.NewClientTLSFromFile( + certPath, "", + ) + if err != nil { + return nil, nil, nil, err + } + + serverCreds := credentials.NewTLS(tlsCfg) + serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} + + // For our REST dial options, we'll still use TLS, but also increase + // the max message size that we'll decode to allow clients to hit + // endpoints which return more data such as the DescribeGraph call. + // We set this to 200MiB atm. Should be the same value as maxMsgRecvSize + // in cmd/lncli/main.go. + restDialOpts := []grpc.DialOption{ + grpc.WithTransportCredentials(restCreds), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize), + ), + } + + // Return a function closure that can be used to listen on a given + // address with the current TLS config. + restListen := func(addr net.Addr) (net.Listener, error) { + // For restListen we will call ListenOnAddress if TLS is + // disabled. + if t.cfg.DisableRestTLS { + return lncfg.ListenOnAddress(addr) + } + + return lncfg.TLSListenOnAddress(addr, tlsCfg) + } + + return serverOpts, restDialOpts, restListen, nil +} + +// generateOrRenewCert generates a new TLS certificate if we're not using one +// yet or renews it if it's outdated. +func (t *TLSManager) generateOrRenewCert() (*tls.Config, func(), error) { + // Generete a TLS pair if we don't have one yet. + var emptyKeyRing keychain.SecretKeyRing + err := t.generateCertPair(emptyKeyRing) + if err != nil { + return nil, nil, err + } + + certData, parsedCert, err := cert.LoadCert( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + if err != nil { + return nil, nil, err + } + + // Check to see if the certificate needs to be renewed. If it does, we + // return the newly generated certificate data instead. + reloadedCertData, err := t.maintainCert(parsedCert) + if err != nil { + return nil, nil, err + } + if reloadedCertData != nil { + certData = *reloadedCertData + } + + tlsCfg := cert.TLSConfFromCert(certData) + cleanUp := t.setUpLetsEncrypt(&certData, tlsCfg) + + return tlsCfg, cleanUp, nil +} + +// generateCertPair creates and writes a TLS pair to disk if the pair +// doesn't exist yet. If the TLSEncryptKey setting is on, and a plaintext key +// is already written to disk, this function overwrites the plaintext key with +// the encrypted form. +func (t *TLSManager) generateCertPair(keyRing keychain.SecretKeyRing) error { + // Ensure we create TLS key and certificate if they don't exist. + if lnrpc.FileExists(t.cfg.TLSCertPath) || + lnrpc.FileExists(t.cfg.TLSKeyPath) { + + // Handle discrepencies related to the TLSEncryptKey setting. + return t.ensureEncryption(keyRing) + } + + rpcsLog.Infof("Generating TLS certificates...") + certBytes, keyBytes, err := cert.GenCertPair( + "lnd autogenerated cert", t.cfg.TLSCertPath, + t.cfg.TLSKeyPath, t.cfg.TLSExtraIPs, + t.cfg.TLSExtraDomains, t.cfg.TLSDisableAutofill, + t.cfg.TLSCertDuration, + ) + if err != nil { + return err + } + + if t.cfg.TLSEncryptKey { + var b bytes.Buffer + e, err := lnencrypt.KeyRingEncrypter(keyRing) + if err != nil { + return fmt.Errorf("unable to create "+ + "encrypt key %v", err) + } + + err = e.EncryptPayloadToWriter( + keyBytes, &b, + ) + if err != nil { + return err + } + + keyBytes = b.Bytes() + } + + err = cert.WriteCertPair( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, certBytes, keyBytes, + ) + + rpcsLog.Infof("Done generating TLS certificates") + + return err +} + +// ensureEncryption takes a look at a couple of things: +// 1) If the TLS key is in plaintext, but TLSEncryptKey is set, we need to +// encrypt the file and rewrite it to disk. +// 2) On the flip side, if TLSEncryptKey is not set, but the key on disk +// is encrypted, we need to error out and warn the user. +func (t *TLSManager) ensureEncryption(keyRing keychain.SecretKeyRing) error { + _, keyBytes, err := cert.GetCertBytesFromPath( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + if err != nil { + return err + } + + if t.cfg.TLSEncryptKey && bytes.HasPrefix(keyBytes, privateKeyPrefix) { + var b bytes.Buffer + e, err := lnencrypt.KeyRingEncrypter(keyRing) + if err != nil { + return fmt.Errorf("unable to generate encrypt key %w", + err) + } + + err = e.EncryptPayloadToWriter(keyBytes, &b) + if err != nil { + return err + } + err = ioutil.WriteFile( + t.cfg.TLSKeyPath, b.Bytes(), modifyFilePermissions, + ) + if err != nil { + return err + } + } + + // If the private key is encrypted but the user didn't pass + // --tlsencryptkey we error out. This is because the wallet is not + // unlocked yet and we don't have access to the keys yet for decryption. + if !t.cfg.TLSEncryptKey && !bytes.HasPrefix(keyBytes, + privateKeyPrefix) { + + ltndLog.Errorf("The TLS private key is encrypted on disk.") + + return errors.New("the TLS key is encrypted but the " + + "--tlsencryptkey flag is not passed. Please either " + + "restart lnd with the --tlsencryptkey flag or delete " + + "the TLS files for regeneration") + } + + return nil +} + +// decryptTLSKeyBytes decrypts the TLS key. +func decryptTLSKeyBytes(keyRing keychain.SecretKeyRing, + encryptedData []byte) ([]byte, error) { + + reader := bytes.NewReader(encryptedData) + encrypter, err := lnencrypt.KeyRingEncrypter(keyRing) + if err != nil { + return nil, err + } + + plaintext, err := encrypter.DecryptPayloadFromReader( + reader, + ) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +// maintainCert checks if the certificate IP and domains matches the config, +// and renews the certificate if either this data is outdated or the +// certificate is expired. +func (t *TLSManager) maintainCert( + parsedCert *x509.Certificate) (*tls.Certificate, error) { + + // We check whether the certificate we have on disk match the IPs and + // domains specified by the config. If the extra IPs or domains have + // changed from when the certificate was created, we will refresh the + // certificate if auto refresh is active. + refresh := false + var err error + if t.cfg.TLSAutoRefresh { + refresh, err = cert.IsOutdated( + parsedCert, t.cfg.TLSExtraIPs, + t.cfg.TLSExtraDomains, t.cfg.TLSDisableAutofill, + ) + if err != nil { + return nil, err + } + } + + // If the certificate expired or it was outdated, delete it and the TLS + // key and generate a new pair. + if !time.Now().After(parsedCert.NotAfter) && !refresh { + return nil, nil + } + + ltndLog.Info("TLS certificate is expired or outdated, " + + "generating a new one") + + err = os.Remove(t.cfg.TLSCertPath) + if err != nil { + return nil, err + } + + err = os.Remove(t.cfg.TLSKeyPath) + if err != nil { + return nil, err + } + + rpcsLog.Infof("Renewing TLS certificates...") + certBytes, keyBytes, err := cert.GenCertPair( + "lnd autogenerated cert", t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + t.cfg.TLSExtraIPs, t.cfg.TLSExtraDomains, + t.cfg.TLSDisableAutofill, t.cfg.TLSCertDuration, + ) + if err != nil { + return nil, err + } + + err = cert.WriteCertPair( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, certBytes, keyBytes, + ) + if err != nil { + return nil, err + } + + rpcsLog.Infof("Done renewing TLS certificates") + + // Reload the certificate data. + reloadedCertData, _, err := cert.LoadCert( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + + return &reloadedCertData, err +} + +// setUpLetsEncrypt automatically generates a Let's Encrypt certificate if the +// option is set. +func (t *TLSManager) setUpLetsEncrypt(certData *tls.Certificate, + tlsCfg *tls.Config) func() { + + // If Let's Encrypt is enabled, instantiate autocert to request/renew + // the certificates. + cleanUp := func() {} + if t.cfg.LetsEncryptDomain == "" { + return cleanUp + } + + ltndLog.Infof("Using Let's Encrypt certificate for domain %v", + t.cfg.LetsEncryptDomain) + + manager := autocert.Manager{ + Cache: autocert.DirCache(t.cfg.LetsEncryptDir), + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist( + t.cfg.LetsEncryptDomain, + ), + } + + srv := &http.Server{ + Addr: t.cfg.LetsEncryptListen, + Handler: manager.HTTPHandler(nil), + ReadHeaderTimeout: letsEncryptTimeout, + } + shutdownCompleted := make(chan struct{}) + cleanUp = func() { + err := srv.Shutdown(context.Background()) + if err != nil { + ltndLog.Errorf("Autocert listener shutdown "+ + " error: %v", err) + + return + } + <-shutdownCompleted + ltndLog.Infof("Autocert challenge listener stopped") + } + + go func() { + ltndLog.Infof("Autocert challenge listener started "+ + "at %v", t.cfg.LetsEncryptListen) + + err := srv.ListenAndServe() + if err != http.ErrServerClosed { + ltndLog.Errorf("autocert http: %v", err) + } + close(shutdownCompleted) + }() + + getCertificate := func(h *tls.ClientHelloInfo) ( + *tls.Certificate, error) { + + lecert, err := manager.GetCertificate(h) + if err != nil { + ltndLog.Errorf("GetCertificate: %v", err) + return certData, nil + } + + return lecert, err + } + + // The self-signed tls.cert remains available as fallback. + tlsCfg.GetCertificate = getCertificate + + return cleanUp +} + +// SetCertificateBeforeUnlock takes care of loading the certificate before +// the wallet is unlocked. If the TLSEncryptKey setting is on, we need to +// generate an ephemeral certificate we're able to use until the wallet is +// unlocked and a new TLS pair can be encrypted to disk. Otherwise we can +// process the certificate normally. +func (t *TLSManager) SetCertificateBeforeUnlock() ([]grpc.ServerOption, + []grpc.DialOption, func(net.Addr) (net.Listener, error), func(), + error) { + + var cleanUp func() + if t.cfg.TLSEncryptKey { + _, err := t.loadEphemeralCertificate() + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to load "+ + "ephemeral certificate: %v", err) + } + } else { + _, cleanUpFunc, err := t.generateOrRenewCert() + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to "+ + "generate or renew TLS certificate: %v", err) + } + cleanUp = cleanUpFunc + } + + serverOpts, restDialOpts, restListen, err := t.getConfig() + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to load TLS "+ + "credentials: %v", err) + } + + return serverOpts, restDialOpts, restListen, cleanUp, nil +} + +// loadEphemeralCertificate creates and loads the ephemeral certificate which +// is used temporarily for secure communications before the wallet is unlocked. +func (t *TLSManager) loadEphemeralCertificate() ([]byte, error) { + rpcsLog.Infof("Generating ephemeral TLS certificates...") + + tmpValidity := validityHours * time.Hour + // Append .tmp to the end of the cert for differentiation. + tmpCertPath := t.cfg.TLSCertPath + ".tmp" + + // Pass in a blank string for the key path so the + // function doesn't write them to disk. + certBytes, keyBytes, err := cert.GenCertPair( + "lnd ephemeral autogenerated cert", tmpCertPath, + "", t.cfg.TLSExtraIPs, t.cfg.TLSExtraDomains, + t.cfg.TLSDisableAutofill, tmpValidity, + ) + if err != nil { + return nil, err + } + t.setEphemeralSettings(keyBytes, certBytes, t.cfg.TLSCertPath+".tmp") + + err = cert.WriteCertPair(tmpCertPath, "", certBytes, keyBytes) + if err != nil { + return nil, err + } + + rpcsLog.Infof("Done generating ephemeral TLS certificates") + + return keyBytes, nil +} + +// LoadPermanentCertificate deletes the ephemeral certificate file and +// generates a new one with the real keyring. +func (t *TLSManager) LoadPermanentCertificate( + keyRing keychain.SecretKeyRing) error { + + if !t.cfg.TLSEncryptKey { + return nil + } + + tmpCertPath := t.cfg.TLSCertPath + ".tmp" + err := os.Remove(tmpCertPath) + if err != nil { + ltndLog.Warn("Unable to delete temp cert at %v", + tmpCertPath) + } + + err = t.generateCertPair(keyRing) + if err != nil { + return err + } + + certBytes, encryptedKeyBytes, err := cert.GetCertBytesFromPath( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + if err != nil { + return err + } + + reader := bytes.NewReader(encryptedKeyBytes) + e, err := lnencrypt.KeyRingEncrypter(keyRing) + if err != nil { + return fmt.Errorf("unable to generate encrypt key %w", + err) + } + + keyBytes, err := e.DecryptPayloadFromReader(reader) + if err != nil { + return err + } + + // Switch the server's TLS certificate to the persistent one. By + // changing the cert data the TLSReloader points to, + err = t.tlsReloader.AttemptReload(certBytes, keyBytes) + if err != nil { + return err + } + + t.deleteEphemeralSettings() + + return nil +} + +// setEphemeralSettings sets the TLSManager settings needed when an ephemeral +// certificate is created. +func (t *TLSManager) setEphemeralSettings(keyBytes, certBytes []byte, + certPath string) { + + t.ephemeralKey = keyBytes + t.ephemeralCert = certBytes + t.ephemeralCertPath = t.cfg.TLSCertPath + ".tmp" +} + +// deleteEphemeralSettings deletes the TLSManager ephemeral settings that are +// no longer needed when the ephemeral certificate is deleted so the Manager +// knows we're no longer using it. +func (t *TLSManager) deleteEphemeralSettings() { + t.ephemeralKey = nil + t.ephemeralCert = nil + t.ephemeralCertPath = "" +} + +// IsCertExpired checks if the current TLS certificate is expired. +func (t *TLSManager) IsCertExpired(keyRing keychain.SecretKeyRing) (bool, + time.Time, error) { + + certBytes, keyBytes, err := cert.GetCertBytesFromPath( + t.cfg.TLSCertPath, t.cfg.TLSKeyPath, + ) + if err != nil { + return false, time.Time{}, err + } + + // If TLSEncryptKey is set, there are two states the + // certificate can be in: ephemeral or permanent. + // Retrieve the key depending on which state it is in. + if t.ephemeralKey != nil { + keyBytes = t.ephemeralKey + } else if t.cfg.TLSEncryptKey { + keyBytes, err = decryptTLSKeyBytes(keyRing, keyBytes) + if err != nil { + return false, time.Time{}, err + } + } + + _, parsedCert, err := cert.LoadCertFromBytes( + certBytes, keyBytes, + ) + if err != nil { + return false, time.Time{}, err + } + + // If the current time is passed the certificate's + // expiry time, then it is considered expired + if time.Now().After(parsedCert.NotAfter) { + return true, parsedCert.NotAfter, nil + } + + return false, parsedCert.NotAfter, nil +} diff --git a/tls_manager_test.go b/tls_manager_test.go new file mode 100644 index 000000000..fcd2eadbf --- /dev/null +++ b/tls_manager_test.go @@ -0,0 +1,371 @@ +package lnd + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io/ioutil" + "math/big" + "net" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/cert" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lntest/channels" + "github.com/lightningnetwork/lnd/lntest/mock" + "github.com/stretchr/testify/require" +) + +const ( + testTLSCertDuration = 42 * time.Hour +) + +var ( + privKeyBytes = channels.AlicesPrivKey + + privKey, _ = btcec.PrivKeyFromBytes(privKeyBytes) +) + +// TestGenerateOrRenewCert creates an expired TLS certificate, to test that a +// new TLS certificate pair is regenerated when the old pair expires. This is +// necessary because the pair expires after a little over a year. +func TestGenerateOrRenewCert(t *testing.T) { + t.Parallel() + + // Write an expired certificate to disk. + certPath, keyPath, expiredCert := writeTestCertFiles( + t, true, false, nil, + ) + + // Now let's run the TLSManager's getConfig. If it works properly, it + // should delete the cert and create a new one. + cfg := &TLSManagerCfg{ + TLSCertPath: certPath, + TLSKeyPath: keyPath, + TLSCertDuration: testTLSCertDuration, + } + tlsManager := NewTLSManager(cfg) + _, cleanUp, err := tlsManager.generateOrRenewCert() + require.NoError(t, err) + _, _, _, err = tlsManager.getConfig() + require.NoError(t, err, "couldn't retrieve TLS config") + t.Cleanup(cleanUp) + + // Grab the certificate to test that getTLSConfig did its job correctly + // and generated a new cert. + newCertData, err := tls.LoadX509KeyPair(certPath, keyPath) + require.NoError(t, err, "couldn't grab new certificate") + + newCert, err := x509.ParseCertificate(newCertData.Certificate[0]) + require.NoError(t, err, "couldn't parse new certificate") + + // Check that the expired certificate was successfully deleted and + // replaced with a new one. + require.True(t, newCert.NotAfter.After(expiredCert.NotAfter), + "New certificate expiration is too old") +} + +// TestTLSManagerGenCert tests that the new TLS Manager loads correctly, +// whether the encrypted TLS key flag is set or not. +func TestTLSManagerGenCert(t *testing.T) { + t.Parallel() + + _, certPath, keyPath := newTestDirectory(t) + + cfg := &TLSManagerCfg{ + TLSCertPath: certPath, + TLSKeyPath: keyPath, + } + tlsManager := NewTLSManager(cfg) + + _, _, err := tlsManager.generateOrRenewCert() + require.NoError(t, err, "failed to generate new certificate") + + // After this is run, a new certificate should be created and written + // to disk. Since the TLSEncryptKey flag isn't set, we should be able + // to read it in plaintext from disk. + _, keyBytes, err := cert.GetCertBytesFromPath( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + require.NoError(t, err, "unable to load certificate") + require.True(t, bytes.HasPrefix(keyBytes, privateKeyPrefix), + "key is encrypted, but shouldn't be") + + // Now test that if the TLSEncryptKey flag is set, an encrypted key is + // created and written to disk. + _, certPath, keyPath = newTestDirectory(t) + + cfg = &TLSManagerCfg{ + TLSEncryptKey: true, + TLSCertPath: certPath, + TLSKeyPath: keyPath, + TLSCertDuration: testTLSCertDuration, + } + tlsManager = NewTLSManager(cfg) + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } + + err = tlsManager.generateCertPair(keyRing) + require.NoError(t, err, "failed to generate new certificate") + + _, keyBytes, err = cert.GetCertBytesFromPath( + certPath, keyPath, + ) + require.NoError(t, err, "unable to load certificate") + require.False(t, bytes.HasPrefix(keyBytes, privateKeyPrefix), + "key isn't encrypted, but should be") +} + +// TestEnsureEncryption tests that ensureEncryption does a couple of things: +// 1) If we have cfg.TLSEncryptKey set, but the tls file saved to disk is not +// encrypted, generateOrRenewCert encrypts the file and rewrites it to disk. +// 2) If cfg.TLSEncryptKey is not set, but the file *is* encrypted, then we +// need to return an error to the user. +func TestEnsureEncryption(t *testing.T) { + t.Parallel() + + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } + + // Write an unencrypted cert file to disk. + certPath, keyPath, _ := writeTestCertFiles( + t, false, false, keyRing, + ) + + cfg := &TLSManagerCfg{ + TLSEncryptKey: true, + TLSCertPath: certPath, + TLSKeyPath: keyPath, + } + tlsManager := NewTLSManager(cfg) + + // Check that the keyBytes are initially plaintext. + _, newKeyBytes, err := cert.GetCertBytesFromPath( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + + require.NoError(t, err, "unable to load certificate files") + require.True(t, bytes.HasPrefix(newKeyBytes, privateKeyPrefix), + "key doesn't have correct plaintext prefix") + + // ensureEncryption should detect that the TLS key is in plaintext, + // encrypt it, and rewrite the encrypted version to disk. + err = tlsManager.ensureEncryption(keyRing) + require.NoError(t, err, "failed to generate new certificate") + + // Grab the file from disk to check that the key is no longer + // plaintext. + _, newKeyBytes, err = cert.GetCertBytesFromPath( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + require.NoError(t, err, "unable to load certificate") + require.False(t, bytes.HasPrefix(newKeyBytes, privateKeyPrefix), + "key isn't encrypted, but should be") + + // Now let's flip the cfg.TLSEncryptKey to false. Since the key on file + // is encrypted, ensureEncryption should error out. + tlsManager.cfg.TLSEncryptKey = false + err = tlsManager.ensureEncryption(keyRing) + require.Error(t, err) +} + +// TestGenerateEphemeralCert tests that an ephemeral certificate is created and +// stored to disk in a .tmp file and that LoadPermanentCertificate deletes +// file and replaces it with a fresh certificate pair. +func TestGenerateEphemeralCert(t *testing.T) { + t.Parallel() + + _, certPath, keyPath := newTestDirectory(t) + tmpCertPath := certPath + ".tmp" + + cfg := &TLSManagerCfg{ + TLSCertPath: certPath, + TLSKeyPath: keyPath, + TLSEncryptKey: true, + TLSCertDuration: testTLSCertDuration, + } + tlsManager := NewTLSManager(cfg) + + keyBytes, err := tlsManager.loadEphemeralCertificate() + require.NoError(t, err, "failed to generate new certificate") + + certBytes, err := ioutil.ReadFile(tmpCertPath) + require.NoError(t, err) + + tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) + require.NoError(t, err) + tlsManager.tlsReloader = tlsr + + // Make sure .tmp file is created at the tmp cert path. + _, err = ioutil.ReadFile(tmpCertPath) + require.NoError(t, err, "couldn't find temp cert file") + + // But no key should be stored. + _, err = ioutil.ReadFile(cfg.TLSKeyPath) + require.Error(t, err, "shouldn't have found file") + + // And no permanent cert file should be stored. + _, err = ioutil.ReadFile(cfg.TLSCertPath) + require.Error(t, err, "shouldn't have found a permanent cert file") + + // Now test that when we reload the certificate it generates the new + // certificate properly. + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } + err = tlsManager.LoadPermanentCertificate(keyRing) + require.NoError(t, err, "unable to reload certificate") + + // Make sure .tmp file is deleted. + _, _, err = cert.GetCertBytesFromPath( + tmpCertPath, cfg.TLSKeyPath, + ) + require.Error(t, err, ".tmp file should have been deleted") + + // Make sure a certificate now exists at the permanent cert path. + _, _, err = cert.GetCertBytesFromPath( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + require.NoError(t, err, "error loading permanent certificate") +} + +// genCertPair generates a key/cert pair, with the option of generating expired +// certificates to make sure they are being regenerated correctly. +func genCertPair(t *testing.T, expired bool) ([]byte, []byte) { + t.Helper() + + // Max serial number. + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + + // Generate a serial number that's below the serialNumberLimit. + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err, "failed to generate serial number") + + host := "lightning" + + // Create a simple ip address for the fake certificate. + ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + + dnsNames := []string{host, "unix", "unixpacket"} + + var notBefore, notAfter time.Time + if expired { + notBefore = time.Now().Add(-time.Hour * 24) + notAfter = time.Now() + } else { + notBefore = time.Now() + notAfter = time.Now().Add(time.Hour * 24) + } + + // Construct the certificate template. + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"lnd autogenerated cert"}, + CommonName: host, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + } + + // Generate a private key for the certificate. + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate a private key") + } + + certDerBytes, err := x509.CreateCertificate( + rand.Reader, &template, &template, &priv.PublicKey, priv, + ) + require.NoError(t, err, "failed to create certificate") + + keyBytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err, "unable to encode privkey") + + return certDerBytes, keyBytes +} + +// writeTestCertFiles creates test files and writes them to a temporary testing +// directory. +func writeTestCertFiles(t *testing.T, expiredCert, encryptTLSKey bool, + keyRing keychain.KeyRing) (string, string, *x509.Certificate) { + + t.Helper() + + tempDir, certPath, keyPath := newTestDirectory(t) + + var certDerBytes, keyBytes []byte + // Either create a valid certificate or an expired certificate pair, + // depending on the test. + if expiredCert { + certDerBytes, keyBytes = genCertPair(t, true) + } else { + certDerBytes, keyBytes = genCertPair(t, false) + } + + parsedCert, err := x509.ParseCertificate(certDerBytes) + require.NoError(t, err, "failed to parse certificate") + + certBuf := bytes.Buffer{} + err = pem.Encode( + &certBuf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certDerBytes, + }, + ) + require.NoError(t, err, "failed to encode certificate") + + var keyBuf *bytes.Buffer + if !encryptTLSKey { + keyBuf = &bytes.Buffer{} + err = pem.Encode( + keyBuf, &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + }, + ) + require.NoError(t, err, "failed to encode private key") + } else { + e, err := lnencrypt.KeyRingEncrypter(keyRing) + require.NoError(t, err, "unable to generate key encrypter") + err = e.EncryptPayloadToWriter( + keyBytes, keyBuf, + ) + require.NoError(t, err, "failed to encrypt private key") + } + + err = ioutil.WriteFile(tempDir+"/tls.cert", certBuf.Bytes(), 0644) + require.NoError(t, err, "failed to write cert file") + err = ioutil.WriteFile(tempDir+"/tls.key", keyBuf.Bytes(), 0600) + require.NoError(t, err, "failed to write key file") + + return certPath, keyPath, parsedCert +} + +// newTestDirectory creates a new test directory and returns the location of +// the test tls.cert and tls.key files. +func newTestDirectory(t *testing.T) (string, string, string) { + t.Helper() + + tempDir := t.TempDir() + certPath := tempDir + "/tls.cert" + keyPath := tempDir + "/tls.key" + + return tempDir, certPath, keyPath +}