Merge pull request #6527 from voltagecloud/encrypt-tls-key-2

tls: Add ability to encrypt TLS key on disk
This commit is contained in:
Oliver Gugger 2023-01-27 14:08:09 +01:00 committed by GitHub
commit 5b354c6598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1110 additions and 349 deletions

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

2
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

223
lnd.go
View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -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) {

639
tls_manager.go Normal file
View file

@ -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
}

371
tls_manager_test.go Normal file
View file

@ -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
}