From 0a9ba4b15b98616663d36bd2c7c8792ea5781dcb Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Sat, 11 Jul 2020 22:29:00 -0500 Subject: [PATCH 1/7] lnencrypt: Moves the crypto functions in the chanbackup package into its own package called lnencrypt The functions inside of the crypto.go file in chanbackup (like EncryptPayloadToWriter and DecryptPayloadFromReader) can be used by a lot of things outside of just the chanbackup package. We can't just reference them directly from the chanbackup package because it's likely that it would generate circular dependencies. Therefore we need to move these functions into their own package to be referenced by chanbackup and whatever new functionality that needs them --- chanbackup/backupfile_test.go | 6 ++- chanbackup/multi.go | 5 ++- chanbackup/multi_test.go | 14 ++++-- chanbackup/pubsub_test.go | 14 ++++-- chanbackup/recover_test.go | 15 ++++--- chanbackup/single.go | 13 +++--- chanbackup/single_test.go | 33 ++++++++++++--- keychain/derivation.go | 11 +++-- keychain/interface_test.go | 2 +- {chanbackup => lnencrypt}/crypto.go | 38 ++++++++--------- {chanbackup => lnencrypt}/crypto_test.go | 54 ++++++++++-------------- lntest/mock/secretkeyring.go | 6 +++ 12 files changed, 124 insertions(+), 87 deletions(-) rename {chanbackup => lnencrypt}/crypto.go (78%) rename {chanbackup => lnencrypt}/crypto_test.go (75%) diff --git a/chanbackup/backupfile_test.go b/chanbackup/backupfile_test.go index 19733c360..1bedad0e2 100644 --- a/chanbackup/backupfile_test.go +++ b/chanbackup/backupfile_test.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/lightningnetwork/lnd/lntest/mock" ) func makeFakePackedMulti() (PackedMulti, error) { @@ -188,7 +190,9 @@ func assertMultiEqual(t *testing.T, a, b *Multi) { func TestExtractMulti(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // First, as prep, we'll create a single chan backup, then pack that // fully into a multi backup. diff --git a/chanbackup/multi.go b/chanbackup/multi.go index e90bd613e..e3ea71f13 100644 --- a/chanbackup/multi.go +++ b/chanbackup/multi.go @@ -6,6 +6,7 @@ import ( "io" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnencrypt" "github.com/lightningnetwork/lnd/lnwire" ) @@ -89,7 +90,7 @@ func (m Multi) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error { // With the plaintext multi backup assembled, we'll now encrypt it // directly to the passed writer. - return encryptPayloadToWriter(multiBackupBuffer, w, keyRing) + return lnencrypt.EncryptPayloadToWriter(multiBackupBuffer, w, keyRing) } // UnpackFromReader attempts to unpack (decrypt+deserialize) a packed @@ -99,7 +100,7 @@ func (m *Multi) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error { // We'll attempt to read the entire packed backup, and also decrypt it // using the passed key ring which is expected to be able to derive the // encryption keys. - plaintextBackup, err := decryptPayloadFromReader(r, keyRing) + plaintextBackup, err := lnencrypt.DecryptPayloadFromReader(r, keyRing) if err != nil { return err } diff --git a/chanbackup/multi_test.go b/chanbackup/multi_test.go index a6317e09e..5b5b6bd87 100644 --- a/chanbackup/multi_test.go +++ b/chanbackup/multi_test.go @@ -4,6 +4,9 @@ import ( "bytes" "net" "testing" + + "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lntest/mock" ) // TestMultiPackUnpack... @@ -25,7 +28,9 @@ func TestMultiPackUnpack(t *testing.T) { multi.StaticBackups = append(multi.StaticBackups, single) } - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } versionTestCases := []struct { // version is the pack/unpack version that we should use to @@ -97,7 +102,7 @@ func TestMultiPackUnpack(t *testing.T) { fakeRawMulti := bytes.NewBuffer( bytes.Repeat([]byte{99}, 20), ) - err := encryptPayloadToWriter( + err := lnencrypt.EncryptPayloadToWriter( *fakeRawMulti, &fakePackedMulti, keyRing, ) if err != nil { @@ -122,8 +127,9 @@ func TestMultiPackUnpack(t *testing.T) { func TestPackedMultiUnpack(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} - + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // First, we'll make a new unpacked multi with a random channel. testChannel, err := genRandomOpenChannelShell() if err != nil { diff --git a/chanbackup/pubsub_test.go b/chanbackup/pubsub_test.go index aca52df2d..480bf8241 100644 --- a/chanbackup/pubsub_test.go +++ b/chanbackup/pubsub_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntest/mock" ) type mockSwapper struct { @@ -79,8 +80,9 @@ func (m *mockChannelNotifier) SubscribeChans(chans map[wire.OutPoint]struct{}) ( func TestNewSubSwapperSubscribeFail(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} - + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } var swapper mockSwapper chanNotifier := mockChannelNotifier{ fail: true, @@ -151,7 +153,9 @@ func assertExpectedBackupSwap(t *testing.T, swapper *mockSwapper, func TestSubSwapperIdempotentStartStop(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } var chanNotifier mockChannelNotifier @@ -182,7 +186,9 @@ func TestSubSwapperIdempotentStartStop(t *testing.T) { func TestSubSwapperUpdater(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } chanNotifier := newMockChannelNotifier() swapper := newMockSwapper(keyRing) diff --git a/chanbackup/recover_test.go b/chanbackup/recover_test.go index e2b9d71e3..3a9d73718 100644 --- a/chanbackup/recover_test.go +++ b/chanbackup/recover_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcec" + "github.com/lightningnetwork/lnd/lntest/mock" ) type mockChannelRestorer struct { @@ -48,8 +49,9 @@ func (m *mockPeerConnector) ConnectPeer(node *btcec.PublicKey, func TestUnpackAndRecoverSingles(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} - + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // First, we'll create a number of single chan backups that we'll // shortly back to so we can begin our recovery attempt. numSingles := 10 @@ -124,7 +126,7 @@ func TestUnpackAndRecoverSingles(t *testing.T) { } // If we modify the keyRing, then unpacking should fail. - keyRing.fail = true + keyRing.Fail = true err = UnpackAndRecoverSingles( packedBackups, keyRing, &chanRestorer, &peerConnector, ) @@ -140,8 +142,9 @@ func TestUnpackAndRecoverSingles(t *testing.T) { func TestUnpackAndRecoverMulti(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} - + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // First, we'll create a number of single chan backups that we'll // shortly back to so we can begin our recovery attempt. numSingles := 10 @@ -220,7 +223,7 @@ func TestUnpackAndRecoverMulti(t *testing.T) { } // If we modify the keyRing, then unpacking should fail. - keyRing.fail = true + keyRing.Fail = true err = UnpackAndRecoverMulti( packedMulti, keyRing, &chanRestorer, &peerConnector, ) diff --git a/chanbackup/single.go b/chanbackup/single.go index e51700c88..6da6a7487 100644 --- a/chanbackup/single.go +++ b/chanbackup/single.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnencrypt" "github.com/lightningnetwork/lnd/lnwire" ) @@ -280,10 +281,10 @@ func (s *Single) Serialize(w io.Writer) error { // global counter to use as a sequence number for nonces, and want to ensure // that we're able to decrypt these blobs without any additional context. We // derive the key that we use for encryption via a SHA2 operation of the with -// the golden keychain.KeyFamilyStaticBackup base encryption key. We then take -// the serialized resulting shared secret point, and hash it using sha256 to -// obtain the key that we'll use for encryption. When using the AEAD, we pass -// the nonce as associated data such that we'll be able to package the two +// the golden keychain.KeyFamilyBaseEncryption base encryption key. We then +// take the serialized resulting shared secret point, and hash it using sha256 +// to obtain the key that we'll use for encryption. When using the AEAD, we +// pass the nonce as associated data such that we'll be able to package the two // together for storage. Before writing out the encrypted payload, we prepend // the nonce to the final blob. func (s *Single) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error { @@ -298,7 +299,7 @@ func (s *Single) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error { // Finally, we'll encrypt the raw serialized SCB (using the nonce as // associated data), and write out the ciphertext prepend with the // nonce that we used to the passed io.Reader. - return encryptPayloadToWriter(rawBytes, w, keyRing) + return lnencrypt.EncryptPayloadToWriter(rawBytes, w, keyRing) } // readLocalKeyDesc reads a KeyDescriptor encoded within an unpacked Single. @@ -466,7 +467,7 @@ func (s *Single) Deserialize(r io.Reader) error { // payload for whatever reason (wrong key, wrong nonce, etc), then this method // will return an error. func (s *Single) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error { - plaintext, err := decryptPayloadFromReader(r, keyRing) + plaintext, err := lnencrypt.DecryptPayloadFromReader(r, keyRing) if err != nil { return err } diff --git a/chanbackup/single_test.go b/chanbackup/single_test.go index ee20d892f..3884b99fa 100644 --- a/chanbackup/single_test.go +++ b/chanbackup/single_test.go @@ -14,6 +14,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" ) @@ -33,6 +34,18 @@ var ( addr1, _ = net.ResolveTCPAddr("tcp", "10.0.0.2:9000") addr2, _ = net.ResolveTCPAddr("tcp", "10.0.0.3:9000") + + // Use hard-coded keys for Alice and Bob, the two FundingManagers that + // we will test the interaction between. + privKeyBytes = [32]byte{ + 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab, + 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4, + 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, + 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, + } + + privKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), + privKeyBytes[:]) ) func assertSingleEqual(t *testing.T, a, b Single) { @@ -213,7 +226,9 @@ func TestSinglePackUnpack(t *testing.T) { singleChanBackup := NewSingle(channel, []net.Addr{addr1, addr2}) singleChanBackup.RemoteNodePub.Curve = nil - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } versionTestCases := []struct { // version is the pack/unpack version that we should use to @@ -311,7 +326,9 @@ func TestSinglePackUnpack(t *testing.T) { func TestPackedSinglesUnpack(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // To start, we'll create 10 new singles, and them assemble their // packed forms into a slice. @@ -362,7 +379,9 @@ func TestPackedSinglesUnpack(t *testing.T) { func TestSinglePackStaticChanBackups(t *testing.T) { t.Parallel() - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // First, we'll create a set of random single, and along the way, // create a map that will let us look up each single by its chan point. @@ -410,8 +429,9 @@ func TestSinglePackStaticChanBackups(t *testing.T) { // If we attempt to pack again, but force the key ring to fail, then // the entire method should fail. + keyRing.Fail = true _, err = PackStaticChanBackups( - unpackedSingles, &mockKeyRing{true}, + unpackedSingles, keyRing, ) if err == nil { t.Fatalf("pack attempt should fail") @@ -437,8 +457,9 @@ func TestSingleUnconfirmedChannel(t *testing.T) { channel.FundingBroadcastHeight = fundingBroadcastHeight singleChanBackup := NewSingle(channel, []net.Addr{addr1, addr2}) - keyRing := &mockKeyRing{} - + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } // Pack it and then unpack it again to make sure everything is written // correctly, then check that the block height of the unpacked // is the funding broadcast height we set before. diff --git a/keychain/derivation.go b/keychain/derivation.go index b22d8f70f..4408427c7 100644 --- a/keychain/derivation.go +++ b/keychain/derivation.go @@ -84,12 +84,11 @@ const ( // p2p level (BOLT-0008). KeyFamilyNodeKey KeyFamily = 6 - // KeyFamilyStaticBackup is the family of keys that will be used to - // derive keys that we use to encrypt and decrypt our set of static - // backups. These backups may either be stored within watch towers for - // a payment, or self stored on disk in a single file containing all - // the static channel backups. - KeyFamilyStaticBackup KeyFamily = 7 + // KeyFamilyBaseEncryption is the family of keys that will be used to + // derive keys that we use to encrypt and decrypt any general blob data + // like static channel backups and the TLS private key. Often used when + // encrypting files on disk. + KeyFamilyBaseEncryption KeyFamily = 7 // KeyFamilyTowerSession is the family of keys that will be used to // derive session keys when negotiating sessions with watchtowers. The diff --git a/keychain/interface_test.go b/keychain/interface_test.go index 654ad22ce..6cd349fbb 100644 --- a/keychain/interface_test.go +++ b/keychain/interface_test.go @@ -30,7 +30,7 @@ var versionZeroKeyFamilies = []KeyFamily{ KeyFamilyDelayBase, KeyFamilyRevocationRoot, KeyFamilyNodeKey, - KeyFamilyStaticBackup, + KeyFamilyBaseEncryption, KeyFamilyTowerSession, KeyFamilyTowerID, } diff --git a/chanbackup/crypto.go b/lnencrypt/crypto.go similarity index 78% rename from chanbackup/crypto.go rename to lnencrypt/crypto.go index 8fdb46f68..c47b56842 100644 --- a/chanbackup/crypto.go +++ b/lnencrypt/crypto.go @@ -1,4 +1,4 @@ -package chanbackup +package lnencrypt import ( "bytes" @@ -15,20 +15,20 @@ import ( // TODO(roasbeef): interface in front of? // baseEncryptionKeyLoc is the KeyLocator that we'll use to derive the base -// encryption key used for encrypting all static channel backups. We use this -// to then derive the actual key that we'll use for encryption. We do this +// encryption key used for encrypting all payloads. We use this to then +// derive the actual key that we'll use for encryption. We do this // rather than using the raw key, as we assume that we can't obtain the raw // keys, and we don't want to require that the HSM know our target cipher for // encryption. // // TODO(roasbeef): possibly unique encrypt? var baseEncryptionKeyLoc = keychain.KeyLocator{ - Family: keychain.KeyFamilyStaticBackup, + Family: keychain.KeyFamilyBaseEncryption, Index: 0, } -// genEncryptionKey derives the key that we'll use to encrypt all of our static -// channel backups. The key itself, is the sha2 of a base key that we get from +// genEncryptionKey derives the key that we'll use to encrypt all of our files +// that are written to disk. The key itself, is the sha2 of a base key that we get from // the keyring. We derive the key this way as we don't force the HSM (or any // future abstractions) to be able to derive and know of the cipher that we'll // use within our protocol. @@ -50,18 +50,18 @@ func genEncryptionKey(keyRing keychain.KeyRing) ([]byte, error) { return encryptionKey[:], nil } -// encryptPayloadToWriter attempts to write the set of bytes contained within +// EncryptPayloadToWriter attempts to write the set of bytes contained within // the passed byes.Buffer into the passed io.Writer in an encrypted form. We // use a 24-byte chachapoly AEAD instance with a randomized nonce that's // pre-pended to the final payload and used as associated data in the AEAD. We // use the passed keyRing to generate the encryption key, see genEncryptionKey // for further details. -func encryptPayloadToWriter(payload bytes.Buffer, w io.Writer, +func EncryptPayloadToWriter(payload bytes.Buffer, w io.Writer, keyRing keychain.KeyRing) error { // First, we'll derive the key that we'll use to encrypt the payload - // for safe storage without giving away the details of any of our - // channels. The final operation is: + // for safe storage without giving away the details of any sensitive + // details. The final operation is: // // key = SHA256(baseKey) encryptionKey, err := genEncryptionKey(keyRing) @@ -96,15 +96,14 @@ func encryptPayloadToWriter(payload bytes.Buffer, w io.Writer, return nil } -// decryptPayloadFromReader attempts to decrypt the encrypted bytes within the +// DecryptPayloadFromReader attempts to decrypt the encrypted bytes within the // passed io.Reader instance using the key derived from the passed keyRing. For // further details regarding the key derivation protocol, see the // genEncryptionKey method. -func decryptPayloadFromReader(payload io.Reader, +func DecryptPayloadFromReader(payload io.Reader, keyRing keychain.KeyRing) ([]byte, error) { - // First, we'll re-generate the encryption key that we use for all the - // SCBs. + // First, we'll re-generate the encryption key. encryptionKey, err := genEncryptionKey(keyRing) if err != nil { return nil, err @@ -112,21 +111,20 @@ func decryptPayloadFromReader(payload io.Reader, // Next, we'll read out the entire blob as we need to isolate the nonce // from the rest of the ciphertext. - packedBackup, err := ioutil.ReadAll(payload) + packedPayload, err := ioutil.ReadAll(payload) if err != nil { return nil, err } - if len(packedBackup) < chacha20poly1305.NonceSizeX { + if len(packedPayload) < chacha20poly1305.NonceSizeX { return nil, fmt.Errorf("payload size too small, must be at "+ "least %v bytes", chacha20poly1305.NonceSizeX) } - nonce := packedBackup[:chacha20poly1305.NonceSizeX] - ciphertext := packedBackup[chacha20poly1305.NonceSizeX:] + nonce := packedPayload[:chacha20poly1305.NonceSizeX] + ciphertext := packedPayload[chacha20poly1305.NonceSizeX:] // Now that we have the cipher text and the nonce separated, we can go - // ahead and decrypt the final blob so we can properly serialized the - // SCB. + // ahead and decrypt the final blob so we can properly serialize. cipher, err := chacha20poly1305.NewX(encryptionKey) if err != nil { return nil, err diff --git a/chanbackup/crypto_test.go b/lnencrypt/crypto_test.go similarity index 75% rename from chanbackup/crypto_test.go rename to lnencrypt/crypto_test.go index 6b4b27fe7..1417f10d9 100644 --- a/chanbackup/crypto_test.go +++ b/lnencrypt/crypto_test.go @@ -1,40 +1,26 @@ -package chanbackup +package lnencrypt import ( "bytes" - "fmt" "testing" "github.com/btcsuite/btcd/btcec" - "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntest/mock" ) var ( - testWalletPrivKey = []byte{ - 0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf, - 0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9, - 0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f, - 0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90, + // Use hard-coded keys for Alice and Bob, the two FundingManagers that + // we will test the interaction between. + privKeyBytes = [32]byte{ + 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab, + 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4, + 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, + 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, } -) - -type mockKeyRing struct { - fail bool -} -func (m *mockKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) { - return keychain.KeyDescriptor{}, nil -} -func (m *mockKeyRing) DeriveKey(keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) { - if m.fail { - return keychain.KeyDescriptor{}, fmt.Errorf("fail") - } - - _, pub := btcec.PrivKeyFromBytes(btcec.S256(), testWalletPrivKey) - return keychain.KeyDescriptor{ - PubKey: pub, - }, nil -} + privKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), + privKeyBytes[:]) +) // TestEncryptDecryptPayload tests that given a static key, we're able to // properly decrypt and encrypted payload. We also test that we'll reject a @@ -81,14 +67,16 @@ func TestEncryptDecryptPayload(t *testing.T) { }, } - keyRing := &mockKeyRing{} + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } for i, payloadCase := range payloadCases { var cipherBuffer bytes.Buffer // First, we'll encrypt the passed payload with our scheme. payloadReader := bytes.NewBuffer(payloadCase.plaintext) - err := encryptPayloadToWriter( + err := EncryptPayloadToWriter( *payloadReader, &cipherBuffer, keyRing, ) if err != nil { @@ -107,7 +95,7 @@ func TestEncryptDecryptPayload(t *testing.T) { cipherBuffer.Write(cipherText) } - plaintext, err := decryptPayloadFromReader(&cipherBuffer, keyRing) + plaintext, err := DecryptPayloadFromReader(&cipherBuffer, keyRing) switch { // If this was meant to be a valid decryption, but we failed, @@ -137,7 +125,9 @@ func TestInvalidKeyEncryption(t *testing.T) { t.Parallel() var b bytes.Buffer - err := encryptPayloadToWriter(b, &b, &mockKeyRing{true}) + keyRing := &mock.SecretKeyRing{} + keyRing.Fail = true + err := EncryptPayloadToWriter(b, &b, keyRing) if err == nil { t.Fatalf("expected error due to fail key gen") } @@ -149,7 +139,9 @@ func TestInvalidKeyDecrytion(t *testing.T) { t.Parallel() var b bytes.Buffer - _, err := decryptPayloadFromReader(&b, &mockKeyRing{true}) + keyRing := &mock.SecretKeyRing{} + keyRing.Fail = true + _, err := DecryptPayloadFromReader(&b, keyRing) if err == nil { t.Fatalf("expected error due to fail key gen") } diff --git a/lntest/mock/secretkeyring.go b/lntest/mock/secretkeyring.go index 1c2da65f0..59d9fa29f 100644 --- a/lntest/mock/secretkeyring.go +++ b/lntest/mock/secretkeyring.go @@ -1,6 +1,8 @@ package mock import ( + "fmt" + "github.com/btcsuite/btcd/btcec" "github.com/lightningnetwork/lnd/keychain" @@ -9,6 +11,7 @@ import ( // SecretKeyRing is a mock implementation of the SecretKeyRing interface. type SecretKeyRing struct { RootKey *btcec.PrivateKey + Fail bool } // DeriveNextKey currently returns dummy values. @@ -23,6 +26,9 @@ func (s *SecretKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) ( // DeriveKey currently returns dummy values. func (s *SecretKeyRing) DeriveKey(keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) { + if s.Fail { + return keychain.KeyDescriptor{}, fmt.Errorf("fail") + } return keychain.KeyDescriptor{ PubKey: s.RootKey.PubKey(), }, nil From 61991bfcb91ce75adbb6f3bf2ed87e2cc7377568 Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Sat, 13 Mar 2021 00:22:13 -0600 Subject: [PATCH 2/7] config+lnd+cert: Add support in lnd for encrypting the TLS private key. This commit adds support in lnd to encrypt the TLS private key on disk with the wallet's seed. This obviously causes issues when the wallet is locked. So for the WalletUnlocker RPC we generate ephemeral TLS certificates with the key stored in memory. This feature is enabled with the --tlsencryptkey flag. --- cert/go.mod | 6 +- cert/go.sum | 296 ++++++++++++++++++++++++++++++++++++++++ cert/selfsigned.go | 51 ++++--- cert/selfsigned_test.go | 214 ++++++++++++++++++++++++++++- cert/tls.go | 63 ++++++++- config.go | 1 + go.sum | 8 ++ lnd.go | 233 ++++++++++++++++++++++++++++--- server.go | 25 +++- server_test.go | 5 +- 10 files changed, 855 insertions(+), 47 deletions(-) diff --git a/cert/go.mod b/cert/go.mod index 858cafcea..9cd790670 100644 --- a/cert/go.mod +++ b/cert/go.mod @@ -2,4 +2,8 @@ module github.com/lightningnetwork/lnd/cert go 1.13 -require github.com/stretchr/testify v1.5.1 +require ( + github.com/btcsuite/btcd v0.21.0-beta + github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20201120035502-0e14e0d904ce + github.com/stretchr/testify v1.5.1 + ) \ No newline at end of file diff --git a/cert/go.sum b/cert/go.sum index 331fa6982..bbb0abad8 100644 --- a/cert/go.sum +++ b/cert/go.sum @@ -1,11 +1,307 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= +github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta.0.20200903105316-61634447e719/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= +github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M= +github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= +github.com/btcsuite/btcwallet v0.11.1-0.20201104022105-a6f3888450e4 h1:iipg6ldTL0y69KLicrd0gLhY/yErB0rG4a84uQbu7ao= +github.com/btcsuite/btcwallet v0.11.1-0.20201104022105-a6f3888450e4/go.mod h1:owv9oZqM0HnUW+ByF7VqOgfs2eb0ooiePW/+Tl/i/Nk= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/walletdb v1.2.0/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc= +github.com/btcsuite/btcwallet/walletdb v1.3.2/go.mod h1:GZCMPNpUu5KE3ASoVd+k06p/1OW8OwNGCCaNWRto2cQ= +github.com/btcsuite/btcwallet/walletdb v1.3.3 h1:u6e7vRIKBF++cJy+hOHaMGg+88ZTwvpaY27AFvtB668= +github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.22+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/lru v1.0.0 h1:Kbsb1SFDsIlaupWPwsPp+dkxiBY1frcS07PCPgotKz8= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= +github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= +github.com/lightninglabs/neutrino v0.11.1-0.20201001005746-884e01dd2d8e h1:cUXdFtH2LPta+hpX8bpKaeEp1qv6dyE5e+gORs5hfo4= +github.com/lightninglabs/neutrino v0.11.1-0.20201001005746-884e01dd2d8e/go.mod h1:MlZmoKa7CJP3eR1s5yB7Rm5aSyadpKkxqAwLQmog7N0= +github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI= +github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= +github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= +github.com/lightningnetwork/lnd v0.0.2 h1:actrQ68Mrj2atPV7A58FxPzP6Qjwvn0GqkxC9iC0Mlw= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20201120035502-0e14e0d904ce h1:2++AGbnyiA4htkw4x5I8OkWQvtTDxYNUlK1iBkZrPzw= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20201120035502-0e14e0d904ce/go.mod h1:GLOw6DTehckKaf1Wdd0qOG53caOytqZ7ounlQVQtyVg= +github.com/lightningnetwork/lnd/cert v1.0.3/go.mod h1:3MWXVLLPI0Mg0XETm9fT4N9Vyy/8qQLmaM5589bEggM= +github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= +github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= +github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= +github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k= +github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg= +github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= +github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= +github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs= +go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= \ No newline at end of file diff --git a/cert/selfsigned.go b/cert/selfsigned.go index 9a41b13d7..c3f1f1e0d 100644 --- a/cert/selfsigned.go +++ b/cert/selfsigned.go @@ -14,6 +14,9 @@ import ( "net" "os" "time" + + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnencrypt" ) const ( @@ -194,8 +197,10 @@ func IsOutdated(cert *x509.Certificate, tlsExtraIPs, return false, nil } -// GenCertPair generates a key/cert pair to the paths provided. The -// auto-generated certificates should *not* be used in production for public +// GenCertPair generates a key/cert pair to the paths provided if defined. +// The bytes of the generated certificate and private key are returned. +// +// The auto-generated certificates should *not* be used in production for public // access as they're self-signed and don't necessarily contain all of the // desired hostnames for the service. For production/public use, consider a // real PKI. @@ -204,7 +209,8 @@ func IsOutdated(cert *x509.Certificate, tlsExtraIPs, // https://github.com/btcsuite/btcutil func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, tlsExtraDomains []string, tlsDisableAutofill bool, - certValidity time.Duration) error { + certValidity time.Duration, encryptKey bool, + keyRing keychain.KeyRing) ([]byte, []byte, error) { now := time.Now() validUntil := now.Add(certValidity) @@ -217,7 +223,7 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, // Generate a serial number that's below the serialNumberLimit. serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - return fmt.Errorf("failed to generate serial number: %s", err) + return nil, nil, fmt.Errorf("failed to generate serial number: %s", err) } // Get all DNS names and IP addresses to use when creating the @@ -225,13 +231,13 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, host, dnsNames := dnsNames(tlsExtraDomains, tlsDisableAutofill) ipAddresses, err := ipAddresses(tlsExtraIPs, tlsDisableAutofill) if err != nil { - return err + return nil, nil, err } // Generate a private key for the certificate. priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return err + return nil, nil, err } // Construct the certificate template. @@ -257,35 +263,46 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { - return fmt.Errorf("failed to create certificate: %v", err) + return nil, nil, fmt.Errorf("failed to create certificate: %v", err) } certBuf := &bytes.Buffer{} err = pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) if err != nil { - return fmt.Errorf("failed to encode certificate: %v", err) + return nil, nil, fmt.Errorf("failed to encode certificate: %v", err) } keybytes, err := x509.MarshalECPrivateKey(priv) if err != nil { - return fmt.Errorf("unable to encode privkey: %v", err) + return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) } keyBuf := &bytes.Buffer{} err = pem.Encode(keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}) if err != nil { - return fmt.Errorf("failed to encode private key: %v", err) + return nil, nil, fmt.Errorf("failed to encode private key: %v", err) } - // Write cert and key files. - if err = ioutil.WriteFile(certFile, certBuf.Bytes(), 0644); err != nil { - return err + // Write cert and key files. Ensures the paths are defined before writing. + if certFile != "" { + if err = ioutil.WriteFile(certFile, certBuf.Bytes(), 0644); err != nil { + return nil, nil, err + } } - if err = ioutil.WriteFile(keyFile, keyBuf.Bytes(), 0600); err != nil { - os.Remove(certFile) - return err + if keyFile != "" { + keyPayload := keyBuf.Bytes() + // If the user requests the TLS key to be encrypted on disk we do so + if encryptKey { + var b bytes.Buffer + lnencrypt.EncryptPayloadToWriter(*keyBuf, &b, keyRing) + keyPayload = b.Bytes() + } + if err = ioutil.WriteFile(keyFile, keyPayload, 0600); err != nil { + os.Remove(certFile) + return nil, nil, err + } } - return nil + return certBuf.Bytes(), keyBuf.Bytes(), nil } diff --git a/cert/selfsigned_test.go b/cert/selfsigned_test.go index b14283490..c9fc7572c 100644 --- a/cert/selfsigned_test.go +++ b/cert/selfsigned_test.go @@ -1,16 +1,30 @@ package cert_test import ( + "bytes" + "fmt" "io/ioutil" "testing" + "github.com/btcsuite/btcd/btcec" "github.com/lightningnetwork/lnd/cert" + "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lntest/mock" "github.com/stretchr/testify/require" ) var ( extraIPs = []string{"1.1.1.1", "123.123.123.1", "199.189.12.12"} extraDomains = []string{"home", "and", "away"} + privKeyBytes = [32]byte{ + 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab, + 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4, + 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, + 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, + } + + privKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), + privKeyBytes[:]) ) // TestIsOutdatedCert checks that we'll consider the TLS certificate outdated @@ -21,13 +35,15 @@ func TestIsOutdatedCert(t *testing.T) { t.Fatal(err) } + keyRing := &mock.SecretKeyRing{} certPath := tempDir + "/tls.cert" keyPath := tempDir + "/tls.key" // Generate TLS files with two extra IPs and domains. - err = cert.GenCertPair( + _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], false, cert.DefaultAutogenValidity, + false, keyRing, ) if err != nil { t.Fatal(err) @@ -37,8 +53,16 @@ func TestIsOutdatedCert(t *testing.T) { // number of IPs and domains. for numIPs := 1; numIPs <= len(extraIPs); numIPs++ { for numDomains := 1; numDomains <= len(extraDomains); numDomains++ { + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } _, parsedCert, err := cert.LoadCert( - certPath, keyPath, + certBytes, keyBytes, ) if err != nil { t.Fatal(err) @@ -76,18 +100,28 @@ func TestIsOutdatedPermutation(t *testing.T) { t.Fatal(err) } + keyRing := &mock.SecretKeyRing{} certPath := tempDir + "/tls.cert" keyPath := tempDir + "/tls.key" // Generate TLS files from the IPs and domains. - err = cert.GenCertPair( + _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:], extraDomains[:], false, cert.DefaultAutogenValidity, + false, keyRing, ) if err != nil { t.Fatal(err) } - _, parsedCert, err := cert.LoadCert(certPath, keyPath) + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } + _, parsedCert, err := cert.LoadCert(certBytes, keyBytes) if err != nil { t.Fatal(err) } @@ -143,21 +177,33 @@ func TestTLSDisableAutofill(t *testing.T) { t.Fatal(err) } + keyRing := &mock.SecretKeyRing{} certPath := tempDir + "/tls.cert" keyPath := tempDir + "/tls.key" // Generate TLS files with two extra IPs and domains and no interface IPs. - err = cert.GenCertPair( + _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], true, cert.DefaultAutogenValidity, + false, keyRing, ) require.NoError( t, err, "unable to generate tls certificate pair", ) + // Read certs from disk + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } + // Load the certificate _, parsedCert, err := cert.LoadCert( - certPath, keyPath, + certBytes, keyBytes, ) require.NoError( t, err, @@ -190,3 +236,159 @@ func TestTLSDisableAutofill(t *testing.T) { "TLS Certificate was not marked as outdated when it should be", ) } + +// TestTlsConfig tests to ensure we can generate a TLS Config from +// a tls cert and tls key. +func TestTlsConfig(t *testing.T) { + tempDir, err := ioutil.TempDir("", "certtest") + if err != nil { + t.Fatal(err) + } + + certPath := tempDir + "/tls.cert" + keyPath := tempDir + "/tls.key" + keyRing := &mock.SecretKeyRing{} + + // Generate TLS files with an extra IP and domain. + _, _, err = cert.GenCertPair( + "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, + []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, + false, keyRing, + ) + if err != nil { + t.Fatal(err) + } + + // Read certs from disk + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } + + // Load the certificate + certData, parsedCert, err := cert.LoadCert( + certBytes, keyBytes, + ) + if err != nil { + t.Fatal(err) + } + + // Check to make sure the IP and domain are in the cert + var foundDomain bool + var foundIp bool + for _, domain := range parsedCert.DNSNames { + if domain == extraDomains[0] { + foundDomain = true + break + } + } + for _, ip := range parsedCert.IPAddresses { + if ip.String() == extraIPs[0] { + foundIp = true + break + } + } + if !foundDomain || !foundIp { + t.Fatal(fmt.Errorf("Did not find required information inside "+ + "of TLS Certificate. foundDomain: %v, foundIp: %v", + foundDomain, foundIp)) + } + + // Create TLS Config + tlsCfg := cert.TLSConfFromCert(certData) + + if len(tlsCfg.Certificates) != 1 { + t.Fatal(fmt.Errorf("Found incorrect number of TLS certificates "+ + "in TLS Config: %v", len(tlsCfg.Certificates))) + } +} + +// TestEncryptedTlsConfig tests to ensure we can generate a TLS Config from +// a tls cert and tls key with key encryption is enabled. +func TestEncryptedTlsConfig(t *testing.T) { + tempDir, err := ioutil.TempDir("", "certtest") + if err != nil { + t.Fatal(err) + } + + certPath := tempDir + "/tls.cert" + keyPath := tempDir + "/tls.key" + keyRing := &mock.SecretKeyRing{ + RootKey: privKey, + } + + // Generate TLS files with an extra IP and domain. + _, _, err = cert.GenCertPair( + "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, + []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, + true, keyRing, + ) + if err != nil { + t.Fatal(err) + } + + // Read certs from disk + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(err) + } + + // Check to make sure the file was written encrypted + privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + if bytes.HasPrefix(keyBytes, privateKeyPrefix) { + t.Fatal(fmt.Errorf("TLS Certificate is written in plaintext when it " + + "should be written encrypted.")) + } + + // Try to decrypt the key + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) + if err != nil { + t.Fatal(err) + } + + // Load the certificate + certData, parsedCert, err := cert.LoadCert( + certBytes, keyBytes, + ) + if err != nil { + t.Fatal(err) + } + + // Check to make sure the IP and domain are in the cert + var foundDomain bool + var foundIp bool + for _, domain := range parsedCert.DNSNames { + if domain == extraDomains[0] { + foundDomain = true + break + } + } + for _, ip := range parsedCert.IPAddresses { + if ip.String() == extraIPs[0] { + foundIp = true + break + } + } + if !foundDomain || !foundIp { + t.Fatal(fmt.Errorf("Did not find required information inside "+ + "of TLS Certificate. foundDomain: %v, foundIp: %v", + foundDomain, foundIp)) + } + + // Create TLS Config + tlsCfg := cert.TLSConfFromCert(certData) + + if len(tlsCfg.Certificates) != 1 { + t.Fatal(fmt.Errorf("Found incorrect number of TLS certificates "+ + "in TLS Config: %v", len(tlsCfg.Certificates))) + } +} diff --git a/cert/tls.go b/cert/tls.go index a8783158e..6d90f2896 100644 --- a/cert/tls.go +++ b/cert/tls.go @@ -3,6 +3,8 @@ package cert import ( "crypto/tls" "crypto/x509" + "io/ioutil" + "sync" ) var ( @@ -24,17 +26,36 @@ var ( } ) +type TlsReloader struct { + certMu sync.RWMutex + cert *tls.Certificate +} + +// GetCertBytesFromPath reads the TLS certificate and key files at the given +// certPath and keyPath and returns the file bytes. +func GetCertBytesFromPath(certPath, keyPath string) (certBytes, keyBytes []byte, err error) { + certBytes, err = ioutil.ReadFile(certPath) + if err != nil { + return nil, nil, err + } + keyBytes, err = ioutil.ReadFile(keyPath) + if err != nil { + return nil, nil, err + } + return certBytes, keyBytes, nil +} + // LoadCert loads a certificate and its corresponding private key from the PEM -// files indicated and returns the certificate in the two formats it is most +// bytes indicated and returns the certificate in the two formats it is most // commonly used. -func LoadCert(certPath, keyPath string) (tls.Certificate, *x509.Certificate, +func LoadCert(certBytes, keyBytes []byte) (tls.Certificate, *x509.Certificate, error) { // The certData returned here is just a wrapper around the PEM blocks // loaded from the file. The PEM is not yet fully parsed but a basic // check is performed that the certificate and private key actually // belong together. - certData, err := tls.LoadX509KeyPair(certPath, keyPath) + certData, err := tls.X509KeyPair(certBytes, keyBytes) if err != nil { return tls.Certificate{}, nil, err } @@ -58,3 +79,39 @@ func TLSConfFromCert(certData tls.Certificate) *tls.Config { MinVersion: tls.VersionTLS12, } } + +// NewTLSReloader is used to create a new TLS Reloader that will be used +// to update the TLS certificate without restarting the server. +func NewTLSReloader(certBytes, keyBytes []byte) (*TlsReloader, error) { + result := &TlsReloader{} + cert, _, err := LoadCert(certBytes, keyBytes) + if err != nil { + return nil, err + } + result.cert = &cert + return result, nil +} + +// AttemptReload will make an attempt to update the TLS certificate +// and key used by the server. +func (tlsr *TlsReloader) AttemptReload(certBytes, keyBytes []byte) error { + newCert, _, err := LoadCert(certBytes, keyBytes) + if err != nil { + return err + } + tlsr.certMu.Lock() + defer tlsr.certMu.Unlock() + tlsr.cert = &newCert + return nil +} + +// GetCertificateFunc is used in the server's TLS configuration to +// determine the correct TLS certificate to server on a request. +func (tlsr *TlsReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) ( + *tls.Certificate, error) { + return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + tlsr.certMu.RLock() + defer tlsr.certMu.RUnlock() + return tlsr.cert, nil + } +} diff --git a/config.go b/config.go index d3ab9b481..cdc6c6914 100644 --- a/config.go +++ b/config.go @@ -191,6 +191,7 @@ type Config struct { TLSExtraDomains []string `long:"tlsextradomain" description:"Adds an extra domain to the generated certificate"` 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"` + 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"` diff --git a/go.sum b/go.sum index bb8808368..de698b35d 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcug github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.20.1-beta.0.20200903105316-61634447e719/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= +github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa h1:sobXG8TE1VEBX4QWOzSKyulSwuOFdb8vzyhGyblXrmQ= github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= @@ -35,6 +37,7 @@ github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2ut github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= +github.com/btcsuite/btcwallet v0.11.1-0.20201104022105-a6f3888450e4/go.mod h1:owv9oZqM0HnUW+ByF7VqOgfs2eb0ooiePW/+Tl/i/Nk= github.com/btcsuite/btcwallet v0.11.1-0.20201207233335-415f37ff11a1 h1:3gvLezYoUkr9MvxocB/vyPNzL+gSqsNT4Q6XTPK+R04= github.com/btcsuite/btcwallet v0.11.1-0.20201207233335-415f37ff11a1/go.mod h1:P1U4LKSB/bhFQdOM7ab1XqNoBGFyFAe7eKObEBD9mIo= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= @@ -44,7 +47,9 @@ github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZw github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s= github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/walletdb v1.2.0/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc= github.com/btcsuite/btcwallet/walletdb v1.3.2/go.mod h1:GZCMPNpUu5KE3ASoVd+k06p/1OW8OwNGCCaNWRto2cQ= +github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/walletdb v1.3.4 h1:ExdPQSfYRLoYMEENsjWyl4w0PePLm9w3wg69nsRS2xc= github.com/btcsuite/btcwallet/walletdb v1.3.4/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= @@ -178,12 +183,14 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= +github.com/lightninglabs/neutrino v0.11.1-0.20201001005746-884e01dd2d8e/go.mod h1:MlZmoKa7CJP3eR1s5yB7Rm5aSyadpKkxqAwLQmog7N0= github.com/lightninglabs/neutrino v0.11.1-0.20201210023533-e1978372d15e h1:K5LCCnSAk3NVT/aCy8wNPv0I5JfyLgijg1VX8Gz306E= github.com/lightninglabs/neutrino v0.11.1-0.20201210023533-e1978372d15e/go.mod h1:KDWfQDKp+CFBxO1t2NRmWuagTY2sYIjpHB1k5vrojTI= github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d h1:QWD/5MPnaZfUVP7P8wLa4M8Td2DI7XXHXt2vhVtUgGI= github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20201120035502-0e14e0d904ce/go.mod h1:GLOw6DTehckKaf1Wdd0qOG53caOytqZ7ounlQVQtyVg= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= @@ -300,6 +307,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/lnd.go b/lnd.go index f1aedc5bc..6d1cea0d4 100644 --- a/lnd.go +++ b/lnd.go @@ -5,6 +5,7 @@ package lnd import ( + "bytes" "context" "crypto/tls" "fmt" @@ -41,6 +42,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnencrypt" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" @@ -264,8 +266,34 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { defer cleanUp() - // Only process macaroons if --no-macaroons isn't set. - serverOpts, restDialOpts, restListen, cleanUp, err := getTLSConfig(cfg) + var serverOpts []grpc.ServerOption + var restDialOpts []grpc.DialOption + var restListen func(net.Addr) (net.Listener, error) + var tlsReloader *cert.TlsReloader + + // The real KeyRing isn't available until after the wallet is unlocked, + // but we need one now. Because we aren't encrypting anything here it can + // be an empty KeyRing. + var emptyKeyRing keychain.KeyRing + // If --tlsencryptkey is set then generate a throwaway TLS pair in memory + // so we can still have TLS even though the wallet isn't unlocked. These + // get thrown away for the real certificates once the wallet is unlocked. + // If TLSEncryptKey is false, then get the TLSConfig like normal. + if cfg.TLSEncryptKey { + serverOpts, + restDialOpts, + restListen, + cleanUp, + tlsReloader, + err = getEphemeralTLSConfig(cfg, emptyKeyRing) + } else { + serverOpts, + restDialOpts, + restListen, + cleanUp, + tlsReloader, + err = getTLSConfig(cfg, emptyKeyRing) + } if err != nil { err := fmt.Errorf("unable to load TLS credentials: %v", err) ltndLog.Error(err) @@ -730,6 +758,39 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { } defer atplManager.Stop() + // If --tlsencryptkey is set, we previously generated a throwaway TLSConfig + // Now we want to remove that and load the persistent TLSConfig + // The wallet is unlocked at this point so we can use the real KeyRing + if cfg.TLSEncryptKey { + tmpCertPath := cfg.TLSCertPath + ".tmp" + err = os.Remove(tmpCertPath) + if err != nil { + ltndLog.Warn("unable to delete temp cert at %v", tmpCertPath) + } + + // Ensure the persistent TLS credentials are created + _, _, _, _, _, err = getTLSConfig(cfg, activeChainControl.KeyRing) + if err != nil { + err := fmt.Errorf("unable to load TLS credentials: %v", err) + ltndLog.Error(err) + return err + } + certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) + if err != nil { + return err + } + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, activeChainControl.KeyRing) + if err != nil { + return err + } + // Switch the server's TLS certificate to the persisntent one + err = tlsReloader.AttemptReload(certBytes, keyBytes) + if err != nil { + return err + } + } + // Now we have created all dependencies necessary to populate and // start the RPC server. err = rpcServer.addDeps( @@ -837,30 +898,144 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { return nil } +// getEphemeralTLSConfig returns a temporary TLS configuration with the TLS +// key and cert for the gRPC server and credentials and a proxy destination +// for the REST reverse proxy. The key is not written to disk. +func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( + []grpc.ServerOption, []grpc.DialOption, + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + + rpcsLog.Infof("Generating ephemeral TLS certificates...") + tmpValidity := 24 * time.Hour + // Append .tmp to the end of the cert for differentiation. + tmpCertPath := 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 temporary autogenerated cert", tmpCertPath, + "", cfg.TLSExtraIPs, cfg.TLSExtraDomains, + cfg.TLSDisableAutofill, tmpValidity, false, keyRing, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + rpcsLog.Infof("Done generating ephemeral TLS certificates") + + certData, _, err := cert.LoadCert( + certBytes, keyBytes, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) + if err != nil { + return nil, nil, nil, nil, nil, err + } + tlsCfg := cert.TLSConfFromCert(certData) + tlsCfg.GetCertificate = tlsr.GetCertificateFunc() + + restCreds, err := credentials.NewClientTLSFromFile(tmpCertPath, "") + if err != nil { + return nil, nil, nil, nil, nil, err + } + + cleanUp := func() {} + 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(1 * 1024 * 1024 * 200), + ), + } + + // 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, tlsr, 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) { +// and a proxy destination for the REST reverse proxy. The cert and key are +// written to disk and the private key can be optionally encrypted. +func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( + []grpc.ServerOption, []grpc.DialOption, + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, 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( + _, _, err := cert.GenCertPair( "lnd autogenerated cert", cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, + cfg.TLSEncryptKey, keyRing, ) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } rpcsLog.Infof("Done generating TLS certificates") } + certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // We check to see if the private key is encrypted or plaintext. + // If it's encrypted we need to try to decrypt it so we can use it + // in the gRPC server. + privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + if !bytes.HasPrefix(keyBytes, privateKeyPrefix) { + // 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 decrypt. + if !cfg.TLSEncryptKey { + return nil, nil, nil, nil, nil, fmt.Errorf("it appears the TLS key is " + + "encrypted but you didn't pass the --tlsencryptkey flag. " + + "Please restart lnd with the --tlsencryptkey flag or delete " + + "the TLS files for regeneration") + } + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } else if cfg.TLSEncryptKey { + // If the user requests an encrypted key but the key is in plaintext + // we encrypt the key before writing to disk. + keyBuf := bytes.NewBuffer(keyBytes) + var b bytes.Buffer + err = lnencrypt.EncryptPayloadToWriter(*keyBuf, &b, keyRing) + if err != nil { + return nil, nil, nil, nil, nil, err + } + if err = ioutil.WriteFile(cfg.TLSKeyPath, b.Bytes(), 0600); err != nil { + return nil, nil, nil, nil, nil, err + } + } + certData, parsedCert, err := cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, + certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } // We check whether the certifcate we have on disk match the IPs and @@ -874,7 +1049,7 @@ func getTLSConfig(cfg *Config) ([]grpc.ServerOption, []grpc.DialOption, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, ) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } } @@ -886,39 +1061,61 @@ func getTLSConfig(cfg *Config) ([]grpc.ServerOption, []grpc.DialOption, err := os.Remove(cfg.TLSCertPath) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } err = os.Remove(cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } rpcsLog.Infof("Renewing TLS certificates...") - err = cert.GenCertPair( + _, _, err = cert.GenCertPair( "lnd autogenerated cert", cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, + cfg.TLSEncryptKey, keyRing, ) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } rpcsLog.Infof("Done renewing TLS certificates") // Reload the certificate data. + certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // If key encryption is set, then decrypt the file. + // We don't need to do a file type check here because GenCertPair + // has been ran with the same value for cfg.TLSEncryptKey. + if cfg.TLSEncryptKey { + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + certData, _, err = cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, + certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } } + tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) + if err != nil { + return nil, nil, nil, nil, nil, err + } tlsCfg := cert.TLSConfFromCert(certData) + tlsCfg.GetCertificate = tlsr.GetCertificateFunc() restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } // If Let's Encrypt is enabled, instantiate autocert to request/renew @@ -1005,7 +1202,7 @@ func getTLSConfig(cfg *Config) ([]grpc.ServerOption, []grpc.DialOption, return lncfg.TLSListenOnAddress(addr, tlsCfg) } - return serverOpts, restDialOpts, restListen, cleanUp, nil + return serverOpts, restDialOpts, restListen, cleanUp, tlsr, nil } // fileExists reports whether the named file or directory exists. diff --git a/server.go b/server.go index ac8ccc3e0..7cf749974 100644 --- a/server.go +++ b/server.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "image/color" + "io/ioutil" "math/big" prand "math/rand" "net" @@ -47,6 +48,7 @@ import ( "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnencrypt" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" @@ -1368,8 +1370,29 @@ func newServer(cfg *Config, listenAddrs []net.Addr, tlsHealthCheck := healthcheck.NewObservation( "tls", func() error { + var emptyKeyRing keychain.KeyRing + certBytes, err := ioutil.ReadFile(cfg.TLSCertPath) + if err != nil { + return err + } + keyBytes, err := ioutil.ReadFile(cfg.TLSKeyPath) + if err != nil { + return err + } + + // If key encryption is set, then decrypt the file. + // We don't need to do a file type check here because GenCertPair + // has been ran with the same value for cfg.TLSEncryptKey. + if cfg.TLSEncryptKey { + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, emptyKeyRing) + if err != nil { + return err + } + } + _, parsedCert, err := cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, + certBytes, keyBytes, ) if err != nil { return err diff --git a/server_test.go b/server_test.go index cfc1b253b..22983fa24 100644 --- a/server_test.go +++ b/server_test.go @@ -17,6 +17,8 @@ import ( "os" "testing" "time" + + "github.com/lightningnetwork/lnd/lntest/mock" ) func TestParseHexColor(t *testing.T) { @@ -119,7 +121,8 @@ func TestTLSAutoRegeneration(t *testing.T) { TLSKeyPath: keyPath, RPCListeners: rpcListeners, } - _, _, _, cleanUp, err := getTLSConfig(cfg) + keyRing := &mock.SecretKeyRing{} + _, _, _, cleanUp, _, err := getTLSConfig(cfg, keyRing) if err != nil { t.Fatalf("couldn't retrieve TLS config") } From 5685da1f4ec72179ca4420e1133324a680534411 Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Sun, 22 Nov 2020 00:13:40 -0600 Subject: [PATCH 3/7] docs: Add documentation for the TLS key encryption flag --- docs/safety.md | 41 +++++++++++++++++++++++++++++++++++++++++ sample-lnd.conf | 3 +++ 2 files changed, 44 insertions(+) diff --git a/docs/safety.md b/docs/safety.md index 6f3d74cc8..7b06c0dea 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -88,6 +88,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 could +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/sample-lnd.conf b/sample-lnd.conf index 2925b5eea..61bc171ce 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -56,6 +56,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. From e73c23e462ecb41698d8245a08731c33373f6cd9 Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Tue, 16 Mar 2021 23:11:13 -0500 Subject: [PATCH 4/7] Add support for external SSL providers. Also adds support for ZeroSSL from the beginning --- cert/selfsigned.go | 63 ++++++--- cert/selfsigned_test.go | 10 +- cert/tls.go | 37 ++++- certprovider/zerossl.go | 289 ++++++++++++++++++++++++++++++++++++++++ config.go | 33 +++++ lnd.go | 266 +++++++++++++++++++++++++++++++++--- 6 files changed, 654 insertions(+), 44 deletions(-) create mode 100644 certprovider/zerossl.go diff --git a/cert/selfsigned.go b/cert/selfsigned.go index c3f1f1e0d..71bb4871c 100644 --- a/cert/selfsigned.go +++ b/cert/selfsigned.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" @@ -16,7 +17,7 @@ import ( "time" "github.com/lightningnetwork/lnd/keychain" - "github.com/lightningnetwork/lnd/lnencrypt" + "github.com/lightningnetwork/lnd/lnencrypt" ) const ( @@ -210,7 +211,7 @@ func IsOutdated(cert *x509.Certificate, tlsExtraIPs, func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, tlsExtraDomains []string, tlsDisableAutofill bool, certValidity time.Duration, encryptKey bool, - keyRing keychain.KeyRing) ([]byte, []byte, error) { + keyRing keychain.KeyRing, keyType string) ([]byte, []byte, error) { now := time.Now() validUntil := now.Add(certValidity) @@ -234,12 +235,6 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, return nil, nil, err } - // Generate a private key for the certificate. - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, nil, err - } - // Construct the certificate template. template := x509.Certificate{ SerialNumber: serialNumber, @@ -260,10 +255,46 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, IPAddresses: ipAddresses, } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, - &template, &priv.PublicKey, priv) - if err != nil { - return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + // Generate a private key for the certificate. + var derBytes []byte + var keyBytes []byte + var encodeString string + if keyType == "ec" { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + derBytes, err = x509.CreateCertificate(rand.Reader, &template, + &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + } + + keyBytes, err = x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) + } + encodeString = "EC PRIVATE KEY" + } else if keyType == "rsa" { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + derBytes, err = x509.CreateCertificate(rand.Reader, &template, + &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %v", err) + } + + keyBytes = x509.MarshalPKCS1PrivateKey(priv) + if err != nil { + return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) + } + encodeString = "RSA PRIVATE KEY" + } else { + return nil, nil, fmt.Errorf("Unknown keyType: %s", keyType) } certBuf := &bytes.Buffer{} @@ -273,13 +304,9 @@ func GenCertPair(org, certFile, keyFile string, tlsExtraIPs, return nil, nil, fmt.Errorf("failed to encode certificate: %v", err) } - keybytes, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return nil, nil, fmt.Errorf("unable to encode privkey: %v", err) - } keyBuf := &bytes.Buffer{} - err = pem.Encode(keyBuf, &pem.Block{Type: "EC PRIVATE KEY", - Bytes: keybytes}) + err = pem.Encode(keyBuf, &pem.Block{Type: encodeString, + Bytes: keyBytes}) if err != nil { return nil, nil, fmt.Errorf("failed to encode private key: %v", err) } diff --git a/cert/selfsigned_test.go b/cert/selfsigned_test.go index c9fc7572c..94ec70671 100644 --- a/cert/selfsigned_test.go +++ b/cert/selfsigned_test.go @@ -43,7 +43,7 @@ func TestIsOutdatedCert(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -108,7 +108,7 @@ func TestIsOutdatedPermutation(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:], extraDomains[:], false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -185,7 +185,7 @@ func TestTLSDisableAutofill(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], extraDomains[:2], true, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) require.NoError( t, err, @@ -253,7 +253,7 @@ func TestTlsConfig(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, - false, keyRing, + false, keyRing, "ec", ) if err != nil { t.Fatal(err) @@ -325,7 +325,7 @@ func TestEncryptedTlsConfig(t *testing.T) { _, _, err = cert.GenCertPair( "lnd autogenerated cert", certPath, keyPath, []string{extraIPs[0]}, []string{extraDomains[0]}, false, cert.DefaultAutogenValidity, - true, keyRing, + true, keyRing, "ec", ) if err != nil { t.Fatal(err) diff --git a/cert/tls.go b/cert/tls.go index 6d90f2896..a28fc5858 100644 --- a/cert/tls.go +++ b/cert/tls.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "io/ioutil" + "strings" "sync" ) @@ -24,6 +25,10 @@ var ( tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, } + tlsRSACipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } ) type TlsReloader struct { @@ -72,12 +77,34 @@ func LoadCert(certBytes, keyBytes []byte) (tls.Certificate, *x509.Certificate, // TLSConfFromCert returns the default TLS configuration used for a server, // using the given certificate as identity. -func TLSConfFromCert(certData tls.Certificate) *tls.Config { - return &tls.Config{ - Certificates: []tls.Certificate{certData}, - CipherSuites: tlsCipherSuites, - MinVersion: tls.VersionTLS12, +func TLSConfFromCert(certData []tls.Certificate) *tls.Config { + var config *tls.Config + + getCertificate := func(h *tls.ClientHelloInfo) (*tls.Certificate, error) { + defaultCertList := []string{"localhost", "127.0.0.1"} + for _, host := range defaultCertList { + if strings.Contains(h.ServerName, host) { + return &certData[0], nil + } + } + return &certData[1], nil + } + + if len(certData) > 1 { + config = &tls.Config{ + Certificates: []tls.Certificate{certData[0]}, + GetCertificate: getCertificate, + CipherSuites: tlsRSACipherSuites, + MinVersion: tls.VersionTLS12, + } + } else { + config = &tls.Config{ + Certificates: []tls.Certificate{certData[0]}, + CipherSuites: tlsCipherSuites, + MinVersion: tls.VersionTLS12, + } } + return config } // NewTLSReloader is used to create a new TLS Reloader that will be used diff --git a/certprovider/zerossl.go b/certprovider/zerossl.go new file mode 100644 index 000000000..d78550739 --- /dev/null +++ b/certprovider/zerossl.go @@ -0,0 +1,289 @@ +package certprovider + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" +) + +var ( + zeroSSLBaseUrl = "https://api.zerossl.com" +) + +type ZeroSSLError struct { + Code string `json:"code"` + Type string `json:"type"` +} + +type ZeroSSLApiError struct { + Success string `json:"success"` + Error ZeroSSLError `json:"error"` +} + +type ZeroSSLValidationMethod struct { + FileValidationUrlHttp string `json:"file_validation_url_http"` + FileValidationUrlHttps string `json:"file_validation_url_https"` + FileValidationContent []string `json:"file_validation_content"` + CnameValidationP1 string `json:"cname_validation_p1"` + CnameValidationP2 string `json:"cname_validation_p2"` +} + +type ZeroSSLValidation struct { + EmailValidation map[string][]string `json:"email_validation"` + OtherValidation map[string]ZeroSSLValidationMethod `json:"other_methods"` +} + +type ZeroSSLExternalCert struct { + Id string `json:"id"` + Type string `json:"type"` + CommonName string `json:"common_name"` + AdditionalDomains string `json:"additional_domains"` + Created string `json:"created"` + Expires string `json:"expires"` + Status string `json:"status"` + ValidationType string `json:"validation_type"` + ValidationEmails string `json:"validation_emails"` + ReplacementFor string `json:"replacement_for"` + Validation ZeroSSLValidation `json:"validation"` +} + +type ZeroSSLCertResponse struct { + Certificate string `json:"certificate.crt"` + CaBundle string `json:"ca_bundle.crt"` +} + +type ZeroSSLCertRevoke struct { + Success int `json:"success"` +} + +func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { + block, _ := pem.Decode(keyBytes) + x509Encoded := block.Bytes + privKey, err := x509.ParsePKCS1PrivateKey(x509Encoded) + if err != nil { + return csrBuffer, err + } + subj := pkix.Name{ + CommonName: domain, + } + rawSubj := subj.ToRDNSequence() + asn1Subj, _ := asn1.Marshal(rawSubj) + template := x509.CertificateRequest{ + RawSubject: asn1Subj, + SignatureAlgorithm: x509.SHA256WithRSA, + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, privKey) + if err != nil { + return csrBuffer, err + } + pem.Encode(&csrBuffer, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) + return csrBuffer, nil +} + +func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return certificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + parsedCsr := strings.Replace(csr.String(), "\n", "", -1) + data := url.Values{} + data.Set("certificate_domains", domain) + data.Set("certificate_validity_days", "90") + data.Set("certificate_csr", parsedCsr) + apiUrl := fmt.Sprintf( + "%s/certificates?access_key=%s", + zeroSSLBaseUrl, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, strings.NewReader(data.Encode())) + if err != nil { + return certificate, err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return certificate, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return certificate, fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, _ := ioutil.ReadAll(resp.Body) + err = json.Unmarshal(body, &certificate) + if err != nil || certificate.Id == "" { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + return certificate, fmt.Errorf("Unknown error occured: %v", string(body)) + } + return certificate, fmt.Errorf("There was a problem requesting a certificate: %v", apiError.Error.Type) + } + return certificate, nil +} + +func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/challenges?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + data := url.Values{} + data.Set("validation_method", "HTTP_CSR_HASH") + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, strings.NewReader(data.Encode())) + if err != nil { + return err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + return nil +} + +func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExternalCert, err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return newCertificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("GET", apiUrl, nil) + if err != nil { + return newCertificate, err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return newCertificate, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return newCertificate, fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return newCertificate, err + } + err = json.Unmarshal(body, &newCertificate) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + fmt.Printf("Unknown error occured: %v\n", string(body)) + } + fmt.Printf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + return newCertificate, nil +} + +func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return "", "", fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/download/return?access_key=%s", + zeroSSLBaseUrl, certificate.Id, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("GET", apiUrl, nil) + if err != nil { + return "", "", err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return "", "", fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + var certResponse ZeroSSLCertResponse + err = json.Unmarshal(body, &certResponse) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + return "", "", fmt.Errorf("Unknown error occured: %v", string(body)) + } + return "", "", fmt.Errorf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + return certResponse.Certificate, certResponse.CaBundle, nil +} + +func ZeroSSLRevokeCert(certificateId string) (err error) { + apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") + if !found { + return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") + } + apiUrl := fmt.Sprintf( + "%s/certificates/%s/revoke?access_key=%s", + zeroSSLBaseUrl, certificateId, apiKey, + ) + client := &http.Client{} + request, err := http.NewRequest("POST", apiUrl, nil) + if err != nil { + return err + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("Received bad response from ZeroSSL: %v - %v", resp.StatusCode, string(body)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + var revokeCert ZeroSSLCertRevoke + err = json.Unmarshal(body, &revokeCert) + if err != nil { + var apiError ZeroSSLApiError + err = json.Unmarshal(body, &apiError) + if err != nil { + fmt.Printf("Unknown error occured: %v\n", string(body)) + return err + } + return fmt.Errorf("There was a problem requesting a certificate: %s", apiError.Error.Type) + } + if revokeCert.Success != 1 { + fmt.Printf("Unknown error occured: %v\n", string(body)) + return fmt.Errorf("There was a problem requesting a certificate: %s", revokeCert) + } + return nil +} diff --git a/config.go b/config.go index cdc6c6914..2ec60f9d8 100644 --- a/config.go +++ b/config.go @@ -55,6 +55,7 @@ const ( defaultRPCPort = 10009 defaultRESTPort = 8080 defaultPeerPort = 9735 + defaultExternalSSLPort = 8787 defaultRPCHost = "localhost" defaultNoSeedBackup = false @@ -206,6 +207,10 @@ type Config struct { LetsEncryptListen string `long:"letsencryptlisten" description:"The IP:port on which lnd will listen for Let's Encrypt challenges. Let's Encrypt will always try to contact on port 80. Often non-root processes are not allowed to bind to ports lower than 1024. This configuration option allows a different port to be used, but must be used in combination with port forwarding from port 80. This configuration can also be used to specify another IP address to listen on, for example an IPv6 address."` LetsEncryptDomain string `long:"letsencryptdomain" description:"Request a Let's Encrypt certificate for this domain. Note that the certicate is only requested and stored when the first rpc connection comes in."` + ExternalSSLProvider string `long:"externalsslprovider" description:"The provider to use when requesting SSL Certificates"` + ExternalSSLPort int `long:"externalsslport" description:"The port on which lnd will listen for certificate validation challenges."` + ExternalSSLDomain string `long:"externalssldomain" description:"Request an external certificate for this domain"` + // We'll parse these 'raw' string arguments into real net.Addrs in the // loadConfig function. We need to expose the 'raw' strings so the // command line library can access them. @@ -358,6 +363,7 @@ func DefaultConfig() Config { TLSKeyPath: defaultTLSKeyPath, LetsEncryptDir: defaultLetsEncryptDir, LetsEncryptListen: defaultLetsEncryptListen, + ExternalSSLPort: defaultExternalSSLPort, LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, @@ -567,6 +573,15 @@ func LoadConfig() (*Config, error) { return cleanCfg, nil } +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + // ValidateConfig check the given configuration to be sane. This makes sure no // illegal values or combination of values are set. All file system paths are // normalized. The cleaned up config is returned on success. @@ -592,6 +607,24 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) { } } + if cfg.ExternalSSLProvider != "" { + if cfg.ExternalSSLDomain == "" { + return nil, fmt.Errorf("you must supply a domain when requesting external certificates") + } + + supportedSSLProviders := []string{"zerossl"} + isSupported := contains(supportedSSLProviders, cfg.ExternalSSLProvider) + if !isSupported { + return nil, fmt.Errorf("Received unsupported external ssl provider: %s", cfg.ExternalSSLProvider) + } + + if cfg.ExternalSSLProvider != "" { + if err := os.MkdirAll(fmt.Sprintf("%s/%s/", lndDir, cfg.ExternalSSLProvider), 0700); err != nil { + return nil, err + } + } + } + funcName := "loadConfig" makeDirectory := func(dir string) error { err := os.MkdirAll(dir, 0700) diff --git a/lnd.go b/lnd.go index 6d1cea0d4..5151bbeb2 100644 --- a/lnd.go +++ b/lnd.go @@ -37,6 +37,7 @@ import ( "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" + "github.com/lightningnetwork/lnd/certprovider" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/channeldb" @@ -270,6 +271,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { var restDialOpts []grpc.DialOption var restListen func(net.Addr) (net.Listener, error) var tlsReloader *cert.TlsReloader + var certId string // The real KeyRing isn't available until after the wallet is unlocked, // but we need one now. Because we aren't encrypting anything here it can @@ -285,6 +287,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { restListen, cleanUp, tlsReloader, + certId, err = getEphemeralTLSConfig(cfg, emptyKeyRing) } else { serverOpts, @@ -763,13 +766,17 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { // The wallet is unlocked at this point so we can use the real KeyRing if cfg.TLSEncryptKey { tmpCertPath := cfg.TLSCertPath + ".tmp" + tmpExternalCertPath := fmt.Sprintf("%s/%s/tls.cert.tmp", cfg.LndDir, cfg.ExternalSSLProvider) err = os.Remove(tmpCertPath) if err != nil { ltndLog.Warn("unable to delete temp cert at %v", tmpCertPath) } - // Ensure the persistent TLS credentials are created - _, _, _, _, _, err = getTLSConfig(cfg, activeChainControl.KeyRing) + err = os.Remove(tmpExternalCertPath) + if err != nil { + ltndLog.Warn("unable to delete temp external cert at %v", tmpExternalCertPath) + } + _, _, _, _, _, err = getTLSConfig(cfg, emptyKeyRing) if err != nil { err := fmt.Errorf("unable to load TLS credentials: %v", err) ltndLog.Error(err) @@ -784,6 +791,11 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { if err != nil { return err } + err = certprovider.ZeroSSLRevokeCert(certId) + if err != nil { + ltndLog.Error("Failed to revoke temporary certifiate:") + ltndLog.Error(err) + } // Switch the server's TLS certificate to the persisntent one err = tlsReloader.AttemptReload(certBytes, keyBytes) if err != nil { @@ -898,46 +910,186 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { return nil } +// createExternalCert creates an Externally provisioned SSL Certificate +func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (returnCert tls.Certificate, certId string, err error) { + var certServer *http.Server + if cfg.ExternalSSLProvider == "zerossl" { + csr, err := certprovider.ZeroSSLGenerateCsr(keyBytes, cfg.ExternalSSLDomain) + if err != nil { + return returnCert, certId, err + } + rpcsLog.Debugf("created csr for %s", cfg.ExternalSSLDomain) + externalCert, err := certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + if err != nil { + return returnCert, certId, err + } + rpcsLog.Infof("received cert request with id %s", externalCert.Id) + domain := externalCert.CommonName + path := externalCert.Validation.OtherValidation[domain].FileValidationUrlHttp + path = strings.Replace(path, "http://"+domain, "", -1) + content := strings.Join(externalCert.Validation.OtherValidation[domain].FileValidationContent[:], "\n") + rpcsLog.Debugf("using cert path: %s", path) + rpcsLog.Debugf("using cert content: %s", content) + go func() { + addr := fmt.Sprintf(":%v", cfg.ExternalSSLPort) + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte(content)) + }) + certServer = &http.Server{ + Addr: addr, + Handler: http.DefaultServeMux, + } + rpcsLog.Infof("starting certificate validator server at %s", + addr) + err := certServer.ListenAndServe() + if err != nil { + rpcsLog.Errorf("there was a problem starting external cert validation server: %v", + err) + return + } + }() + err = certprovider.ZeroSSLValidateCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Debug("requested certificate to be validated") + checkCount := 0 + retries := 0 + for { + newCert, err := certprovider.ZeroSSLGetCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + status := newCert.Status + rpcsLog.Debugf("found certificate in state %s", status) + if status == "issued" { + rpcsLog.Infof("found certificate in state %s", status) + break + } else if status == "draft" { + err = certprovider.ZeroSSLValidateCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + } + if retries > 3 { + rpcsLog.Error("Still can't get a certificate after 3 retries. Failing...") + certServer.Close() + return returnCert, "", fmt.Errorf("Timed out trying to create SSL Certificate") + } + if checkCount > 15 { + rpcsLog.Warn("Timed out waiting for cert. Requesting a new one.") + externalCert, err = certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Infof("received cert request with id %s", externalCert.Id) + retries += 1 + checkCount = 0 + } + checkCount += 1 + time.Sleep(2 * time.Second) + } + certId = externalCert.Id + certificate, caBundle, err := certprovider.ZeroSSLDownloadCert(externalCert) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + externalCertBytes := []byte(certificate + "\n" + caBundle) + if err = ioutil.WriteFile(certLocation, externalCertBytes, 0644); err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Infof("successfully wrote external SSL certificate to %s", + certLocation) + externalCertData, _, err := cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + certServer.Close() + return returnCert, certId, err + } + rpcsLog.Info("shutting down certificate validator server") + certServer.Close() + return externalCertData, certId, nil + } else { + return returnCert, certId, fmt.Errorf("Unknown external certificate provider: %s", cfg.ExternalSSLProvider) + } +} + // getEphemeralTLSConfig returns a temporary TLS configuration with the TLS // key and cert for the gRPC server and credentials and a proxy destination // for the REST reverse proxy. The key is not written to disk. func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( []grpc.ServerOption, []grpc.DialOption, - func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, string, error) { rpcsLog.Infof("Generating ephemeral TLS certificates...") tmpValidity := 24 * time.Hour // Append .tmp to the end of the cert for differentiation. tmpCertPath := cfg.TLSCertPath + ".tmp" + var externalSSLCertPath string + keyType := "ec" + if cfg.ExternalSSLProvider != "" { + keyType = "rsa" + externalSSLCertPath = fmt.Sprintf("%s/%s/tls.cert.tmp", cfg.LndDir, cfg.ExternalSSLProvider) + } + // Pass in a blank string for the key path so the // function doesn't write them to disk. certBytes, keyBytes, err := cert.GenCertPair( "lnd temporary autogenerated cert", tmpCertPath, "", cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, tmpValidity, false, keyRing, + keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } + + var externalCertData tls.Certificate + var certId string + var failedProvision bool + if cfg.ExternalSSLProvider != "" { + externalCertData, certId, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Warn(err) + failedProvision = true + } + } + rpcsLog.Infof("Done generating ephemeral TLS certificates") certData, _, err := cert.LoadCert( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err + } + certList := []tls.Certificate{certData} + if cfg.ExternalSSLProvider != "" && !failedProvision { + certList = append(certList, externalCertData) } - tlsCfg := cert.TLSConfFromCert(certData) + + tlsCfg := cert.TLSConfFromCert(certList) tlsCfg.GetCertificate = tlsr.GetCertificateFunc() restCreds, err := credentials.NewClientTLSFromFile(tmpCertPath, "") if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } cleanUp := func() {} @@ -968,7 +1120,7 @@ func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return lncfg.TLSListenOnAddress(addr, tlsCfg) } - return serverOpts, restDialOpts, restListen, cleanUp, tlsr, nil + return serverOpts, restDialOpts, restListen, cleanUp, tlsr, certId, nil } // getTLSConfig returns a TLS configuration for the gRPC server and credentials @@ -978,6 +1130,14 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( []grpc.ServerOption, []grpc.DialOption, func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + externalSSLCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) + keyType := "ec" + privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + if cfg.ExternalSSLProvider != "" { + keyType = "rsa" + privateKeyPrefix = []byte("-----BEGIN RSA PRIVATE KEY-----") + } + // Ensure we create TLS key and certificate if they don't exist. if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { rpcsLog.Infof("Generating TLS certificates...") @@ -985,11 +1145,19 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( "lnd autogenerated cert", cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, - cfg.TLSEncryptKey, keyRing, + cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { return nil, nil, nil, nil, nil, err } + + // If the external ssl provider is supplied and there was a key rotation + // then we need to rotate the external SSL too. Just delete here so it + // can be regenerated a little farther down + if cfg.ExternalSSLProvider != "" { + os.Remove(externalSSLCertPath) + } + rpcsLog.Infof("Done generating TLS certificates") } @@ -998,10 +1166,9 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } - // We check to see if the private key is encrypted or plaintext. - // If it's encrypted we need to try to decrypt it so we can use it - // in the gRPC server. - privateKeyPrefix := []byte("-----BEGIN EC PRIVATE KEY-----") + // Do a check to see if the TLS private key is encrypted. If it's encrypted, + // try to decrypt it. If it's in plaintext but should be encrypted, + // then encrypt it. if !bytes.HasPrefix(keyBytes, privateKeyPrefix) { // If the private key is encrypted but the user didn't pass // --tlsencryptkey we error out. This is because the wallet is not @@ -1031,6 +1198,35 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( } } + var externalCertData tls.Certificate + var failedProvision bool + if cfg.ExternalSSLProvider != "" { + // Ensure we create external TLS certificate if they don't exist. + if !fileExists(externalSSLCertPath) { + ltndLog.Infof("Requesting external certificate for domain %v", + cfg.ExternalSSLDomain) + _, _, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Info(err) + failedProvision = true + } + } + if !failedProvision { + externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + externalCertData, _, err = cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + } + certData, parsedCert, err := cert.LoadCert( certBytes, keyBytes, ) @@ -1069,12 +1265,19 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } + if cfg.ExternalSSLProvider != "" { + err = os.Remove(externalSSLCertPath) + if err != nil { + return nil, 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, cert.DefaultAutogenValidity, - cfg.TLSEncryptKey, keyRing, + cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { return nil, nil, nil, nil, nil, err @@ -1087,6 +1290,33 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return nil, nil, nil, nil, nil, err } + if cfg.ExternalSSLProvider != "" { + // Ensure we create external TLS certificate if they don't exist. + if !fileExists(externalSSLCertPath) { + ltndLog.Infof("Requesting external certificate for domain %v", + cfg.ExternalSSLDomain) + _, _, err = createExternalCert( + cfg, keyBytes, externalSSLCertPath, + ) + if err != nil { + rpcsLog.Info(err) + failedProvision = true + } + } + if !failedProvision { + externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) + if err != nil { + return nil, nil, nil, nil, nil, err + } + externalCertData, _, err = cert.LoadCert( + externalCertBytes, keyBytes, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + } + // If key encryption is set, then decrypt the file. // We don't need to do a file type check here because GenCertPair // has been ran with the same value for cfg.TLSEncryptKey. @@ -1110,7 +1340,11 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if err != nil { return nil, nil, nil, nil, nil, err } - tlsCfg := cert.TLSConfFromCert(certData) + certList := []tls.Certificate{certData} + if cfg.ExternalSSLProvider != "" && !failedProvision { + certList = append(certList, externalCertData) + } + tlsCfg := cert.TLSConfFromCert(certList) tlsCfg.GetCertificate = tlsr.GetCertificateFunc() restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") From 1ef7b75691fb48b898c0af5507e528b9572bfa2f Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 30 Mar 2021 14:58:56 -0400 Subject: [PATCH 5/7] Add gocron dependencies --- go.mod | 3 ++- go.sum | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8c0133815..e14dbc0ac 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dustin/go-humanize v1.0.0 // indirect + github.com/go-co-op/gocron v1.0.0 // indirect github.com/go-errors/errors v1.0.1 github.com/go-openapi/strfmt v0.19.5 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect @@ -58,7 +59,7 @@ require ( github.com/modern-go/reflect2 v1.0.1 // indirect github.com/prometheus/client_golang v0.9.3 github.com/soheilhy/cmux v0.1.4 // indirect - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.7.0 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 github.com/urfave/cli v1.18.0 diff --git a/go.sum b/go.sum index de698b35d..2b99d8040 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHs github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-co-op/gocron v1.0.0 h1:ALryRuOUIOTFOIqq8ToKS/1nFm6JAjd3rz1o6GXLK8Q= +github.com/go-co-op/gocron v1.0.0/go.mod h1:9rZ2ZJCai6lz7JG5b+AdRsNnxjirM/Vc+gss/FJW8eU= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -249,6 +251,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= @@ -308,6 +312,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -366,6 +372,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 0bbf1f4d337a2662b4466f769eec321b33d0d90a Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 30 Mar 2021 15:16:08 -0400 Subject: [PATCH 6/7] Unit test detecting an expired certificate --- certprovider/zerossl_mock.go | 34 ++++++++++++++++++++++++++++++ server_test.go | 40 +++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 certprovider/zerossl_mock.go diff --git a/certprovider/zerossl_mock.go b/certprovider/zerossl_mock.go new file mode 100644 index 000000000..da4d77707 --- /dev/null +++ b/certprovider/zerossl_mock.go @@ -0,0 +1,34 @@ +package certprovider + +import ( + "bytes" +) + +// MockZeroSSLProvider is a mock implementation of the CertProvider interface. +type MockZeroSSLProvider struct { + Certs map[string]ZeroSSLExternalCert +} + +func (z MockZeroSSLProvider) GenerateCsr(keyBytes []byte, domain string) (bytes.Buffer, error) { + return *bytes.NewBuffer(keyBytes), nil +} + +func (z MockZeroSSLProvider) RequestCert(csr bytes.Buffer, domain string) (ZeroSSLExternalCert, error) { + return ZeroSSLExternalCert{}, nil +} + +func (z MockZeroSSLProvider) ValidateCert(certificate ZeroSSLExternalCert) error { + return nil +} + +func (z *MockZeroSSLProvider) GetCert(certId string) (ZeroSSLExternalCert, error) { + return z.Certs[certId], nil +} + +func (z *MockZeroSSLProvider) DownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { + return "", "", nil +} + +func (z MockZeroSSLProvider) RevokeCert(certificateId string) error { + return nil +} diff --git a/server_test.go b/server_test.go index 22983fa24..2e7eaaedf 100644 --- a/server_test.go +++ b/server_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + "github.com/lightningnetwork/lnd/certprovider" "github.com/lightningnetwork/lnd/lntest/mock" ) @@ -122,7 +123,7 @@ func TestTLSAutoRegeneration(t *testing.T) { RPCListeners: rpcListeners, } keyRing := &mock.SecretKeyRing{} - _, _, _, cleanUp, _, err := getTLSConfig(cfg, keyRing) + _, _, _, cleanUp, _, _, err := getTLSConfig(cfg, keyRing) if err != nil { t.Fatalf("couldn't retrieve TLS config") } @@ -205,3 +206,40 @@ func genExpiredCertPair(t *testing.T, certDirPath string) ([]byte, []byte) { return certDerBytes, keyBytes } + +// TestExpiredCertDetector tests that when ZeroSSL is used, the program checks +// correctly whether the ZeroSSL certificate is expiring soon. +func TestExpiredCertDetector(t *testing.T) { + testCertId := "testid" + + // Create a certificate that expires in 1 day to make sure that our + // detector notices it is expiring soon. + testCert := certprovider.ZeroSSLExternalCert{ + Id: testCertId, + Expires: time.Now().Add(time.Hour * 24 * 2).Format("2006-01-02 15:04:05"), + } + + // Create mock ZeroSSL + zerossl := certprovider.MockZeroSSLProvider{ + Certs: make(map[string]certprovider.ZeroSSLExternalCert), + } + + zerossl.Certs[testCertId] = testCert + + // The certificate is expiring in 2 days. Our expired cert detector + // should notice this. + expired := CheckForExpiredCert(&zerossl, testCertId) + if !expired { + t.Fatalf("failed to detect expiring certificate") + } + + // Also should check that the detector notices when the certificate + // is NOT expiring soon. Try a certificate that's expiring in 4 days. + testCert.Expires = time.Now().Add(time.Hour * 24 * 4).Format("2006-01-02 15:04:05") + zerossl.Certs[testCertId] = testCert + + expired = CheckForExpiredCert(&zerossl, testCertId) + if expired { + t.Fatalf("this certificate is not expiring in three days or less") + } +} From 303ec1e91030edd815f456ced0fb7e6099038f6c Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 30 Mar 2021 15:17:04 -0400 Subject: [PATCH 7/7] Detect an expired certificate and regenerate it --- certprovider/zerossl.go | 25 +++++-- lnd.go | 155 +++++++++++++++++++++++++++++++--------- 2 files changed, 139 insertions(+), 41 deletions(-) diff --git a/certprovider/zerossl.go b/certprovider/zerossl.go index d78550739..a2e44a279 100644 --- a/certprovider/zerossl.go +++ b/certprovider/zerossl.go @@ -66,7 +66,18 @@ type ZeroSSLCertRevoke struct { Success int `json:"success"` } -func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { +type CertProvider interface { + GenerateCsr([]byte, string) (bytes.Buffer, error) + RequestCert(bytes.Buffer, string) (ZeroSSLExternalCert, error) + ValidateCert(ZeroSSLExternalCert) error + GetCert(string) (ZeroSSLExternalCert, error) + DownloadCert(ZeroSSLExternalCert) (string, string, error) + RevokeCert(string) error +} + +type ZeroSSL struct {} + +func (ZeroSSL) GenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, err error) { block, _ := pem.Decode(keyBytes) x509Encoded := block.Bytes privKey, err := x509.ParsePKCS1PrivateKey(x509Encoded) @@ -90,7 +101,7 @@ func ZeroSSLGenerateCsr(keyBytes []byte, domain string) (csrBuffer bytes.Buffer, return csrBuffer, nil } -func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { +func (ZeroSSL) RequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExternalCert, err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return certificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -132,7 +143,7 @@ func ZeroSSLRequestCert(csr bytes.Buffer, domain string) (certificate ZeroSSLExt return certificate, nil } -func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { +func (ZeroSSL) ValidateCert(certificate ZeroSSLExternalCert) error { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -161,14 +172,14 @@ func ZeroSSLValidateCert(certificate ZeroSSLExternalCert) error { return nil } -func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExternalCert, err error) { +func (ZeroSSL) GetCert(certId string) (newCertificate ZeroSSLExternalCert, err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return newCertificate, fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") } apiUrl := fmt.Sprintf( "%s/certificates/%s?access_key=%s", - zeroSSLBaseUrl, certificate.Id, apiKey, + zeroSSLBaseUrl, certId, apiKey, ) client := &http.Client{} request, err := http.NewRequest("GET", apiUrl, nil) @@ -201,7 +212,7 @@ func ZeroSSLGetCert(certificate ZeroSSLExternalCert) (newCertificate ZeroSSLExte return newCertificate, nil } -func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { +func (ZeroSSL) DownloadCert(certificate ZeroSSLExternalCert) (string, string, error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return "", "", fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") @@ -242,7 +253,7 @@ func ZeroSSLDownloadCert(certificate ZeroSSLExternalCert) (string, string, error return certResponse.Certificate, certResponse.CaBundle, nil } -func ZeroSSLRevokeCert(certificateId string) (err error) { +func (ZeroSSL) RevokeCert(certificateId string) (err error) { apiKey, found := os.LookupEnv("ZEROSSL_API_KEY") if !found { return fmt.Errorf("Failed to get the ZEROSSL_API_KEY environment variable. Make sure it's set") diff --git a/lnd.go b/lnd.go index 5151bbeb2..f0d2fe538 100644 --- a/lnd.go +++ b/lnd.go @@ -25,6 +25,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" + "github.com/go-co-op/gocron" proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino/headerfs" @@ -295,6 +296,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { restListen, cleanUp, tlsReloader, + certId, err = getTLSConfig(cfg, emptyKeyRing) } if err != nil { @@ -776,7 +778,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { if err != nil { ltndLog.Warn("unable to delete temp external cert at %v", tmpExternalCertPath) } - _, _, _, _, _, err = getTLSConfig(cfg, emptyKeyRing) + _, _, _, _, _, certId, err = getTLSConfig(cfg, emptyKeyRing) if err != nil { err := fmt.Errorf("unable to load TLS credentials: %v", err) ltndLog.Error(err) @@ -791,18 +793,36 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { if err != nil { return err } - err = certprovider.ZeroSSLRevokeCert(certId) + zerossl := certprovider.ZeroSSL{} + err = zerossl.RevokeCert(certId) if err != nil { ltndLog.Error("Failed to revoke temporary certifiate:") ltndLog.Error(err) } - // Switch the server's TLS certificate to the persisntent one + // Switch the server's TLS certificate to the persistent one err = tlsReloader.AttemptReload(certBytes, keyBytes) if err != nil { return err } } + // If we're using ZeroSSL, we'll spin up a goroutine to check when the certificate expires. + // If it's expiring in three days or less, we'll generate a new certificate using ZeroSSL. + if cfg.ExternalSSLProvider == "zerossl" { + zerossl := certprovider.ZeroSSL{} + + s := gocron.NewScheduler(time.UTC) + + s.Every(1).Day().Do(func(){ + expires := CheckForExpiredCert(zerossl, certId) + if expires { + DeleteAndRegenerateCert(zerossl, cfg, activeChainControl, certId, tlsReloader) + } + }) + + s.StartAsync() + } + // Now we have created all dependencies necessary to populate and // start the RPC server. err = rpcServer.addDeps( @@ -914,12 +934,14 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (returnCert tls.Certificate, certId string, err error) { var certServer *http.Server if cfg.ExternalSSLProvider == "zerossl" { - csr, err := certprovider.ZeroSSLGenerateCsr(keyBytes, cfg.ExternalSSLDomain) + zerossl := certprovider.ZeroSSL{} + + csr, err := zerossl.GenerateCsr(keyBytes, cfg.ExternalSSLDomain) if err != nil { return returnCert, certId, err } rpcsLog.Debugf("created csr for %s", cfg.ExternalSSLDomain) - externalCert, err := certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + externalCert, err := zerossl.RequestCert(csr, cfg.ExternalSSLDomain) if err != nil { return returnCert, certId, err } @@ -950,7 +972,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu return } }() - err = certprovider.ZeroSSLValidateCert(externalCert) + err = zerossl.ValidateCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -959,7 +981,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu checkCount := 0 retries := 0 for { - newCert, err := certprovider.ZeroSSLGetCert(externalCert) + newCert, err := zerossl.GetCert(externalCert.Id) if err != nil { certServer.Close() return returnCert, certId, err @@ -970,7 +992,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu rpcsLog.Infof("found certificate in state %s", status) break } else if status == "draft" { - err = certprovider.ZeroSSLValidateCert(externalCert) + err = zerossl.ValidateCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -983,7 +1005,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu } if checkCount > 15 { rpcsLog.Warn("Timed out waiting for cert. Requesting a new one.") - externalCert, err = certprovider.ZeroSSLRequestCert(csr, cfg.ExternalSSLDomain) + externalCert, err = zerossl.RequestCert(csr, cfg.ExternalSSLDomain) if err != nil { certServer.Close() return returnCert, certId, err @@ -996,7 +1018,7 @@ func createExternalCert(cfg *Config, keyBytes []byte, certLocation string) (retu time.Sleep(2 * time.Second) } certId = externalCert.Id - certificate, caBundle, err := certprovider.ZeroSSLDownloadCert(externalCert) + certificate, caBundle, err := zerossl.DownloadCert(externalCert) if err != nil { certServer.Close() return returnCert, certId, err @@ -1128,7 +1150,7 @@ func getEphemeralTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( // written to disk and the private key can be optionally encrypted. func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( []grpc.ServerOption, []grpc.DialOption, - func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, error) { + func(net.Addr) (net.Listener, error), func(), *cert.TlsReloader, string, error) { externalSSLCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) keyType := "ec" @@ -1148,7 +1170,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // If the external ssl provider is supplied and there was a key rotation @@ -1163,7 +1185,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // Do a check to see if the TLS private key is encrypted. If it's encrypted, @@ -1174,7 +1196,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( // --tlsencryptkey we error out. This is because the wallet is not // unlocked yet and we don't have access to the keys yet for decrypt. if !cfg.TLSEncryptKey { - return nil, nil, nil, nil, nil, fmt.Errorf("it appears the TLS key is " + + return nil, nil, nil, nil, nil, "", fmt.Errorf("it appears the TLS key is " + "encrypted but you didn't pass the --tlsencryptkey flag. " + "Please restart lnd with the --tlsencryptkey flag or delete " + "the TLS files for regeneration") @@ -1182,7 +1204,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( reader := bytes.NewReader(keyBytes) keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } else if cfg.TLSEncryptKey { // If the user requests an encrypted key but the key is in plaintext @@ -1191,21 +1213,22 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( var b bytes.Buffer err = lnencrypt.EncryptPayloadToWriter(*keyBuf, &b, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if err = ioutil.WriteFile(cfg.TLSKeyPath, b.Bytes(), 0600); err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } var externalCertData tls.Certificate var failedProvision bool + var certId string if cfg.ExternalSSLProvider != "" { // Ensure we create external TLS certificate if they don't exist. if !fileExists(externalSSLCertPath) { ltndLog.Infof("Requesting external certificate for domain %v", cfg.ExternalSSLDomain) - _, _, err = createExternalCert( + _, certId, err = createExternalCert( cfg, keyBytes, externalSSLCertPath, ) if err != nil { @@ -1216,13 +1239,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if !failedProvision { externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } externalCertData, _, err = cert.LoadCert( externalCertBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } } @@ -1231,7 +1254,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // We check whether the certifcate we have on disk match the IPs and @@ -1245,7 +1268,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSExtraDomains, cfg.TLSDisableAutofill, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1257,18 +1280,18 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( err := os.Remove(cfg.TLSCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } err = os.Remove(cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if cfg.ExternalSSLProvider != "" { err = os.Remove(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1280,14 +1303,14 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( cfg.TLSEncryptKey, keyRing, keyType, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } rpcsLog.Infof("Done renewing TLS certificates") // Reload the certificate data. certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } if cfg.ExternalSSLProvider != "" { @@ -1306,13 +1329,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( if !failedProvision { externalCertBytes, err := ioutil.ReadFile(externalSSLCertPath) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } externalCertData, _, err = cert.LoadCert( externalCertBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } } @@ -1324,7 +1347,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( reader := bytes.NewReader(keyBytes) keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, keyRing) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } @@ -1332,13 +1355,13 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( certBytes, keyBytes, ) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } } tlsr, err := cert.NewTLSReloader(certBytes, keyBytes) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } certList := []tls.Certificate{certData} if cfg.ExternalSSLProvider != "" && !failedProvision { @@ -1349,7 +1372,7 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, "", err } // If Let's Encrypt is enabled, instantiate autocert to request/renew @@ -1436,7 +1459,71 @@ func getTLSConfig(cfg *Config, keyRing keychain.KeyRing) ( return lncfg.TLSListenOnAddress(addr, tlsCfg) } - return serverOpts, restDialOpts, restListen, cleanUp, tlsr, nil + return serverOpts, restDialOpts, restListen, cleanUp, tlsr, certId, nil +} + +// CheckForExpiredCert finds whether the TLS certificate is expiring soon. +func CheckForExpiredCert(certprovider certprovider.CertProvider, certId string) bool { + cert, err := certprovider.GetCert(certId) + if err != nil { + fmt.Errorf("error retrieving ZeroSSL certificate: %v", err) + } + + // See if the certificate expires in three days or less. + expiresStr := cert.Expires + layout := "2006-01-02 15:04:05" + expiresTime, err := time.Parse(layout, expiresStr) + if err != nil { + fmt.Errorf("error parsing expiry date of certificate: %v", err) + } + + currTime := time.Now() + timeRemaining := expiresTime.Sub(currTime).Hours() + + return timeRemaining < 72 +} + +// DeleteAndRegenerateCert deletes a certificate, either because it was a temporary certificate that is no longer needed, or it's +// about to expire. Then it regenerates a new one and attempts to reload the certificate. +func DeleteAndRegenerateCert(certprovider certprovider.CertProvider, cfg *Config, activeChainControl *chainreg.ChainControl, certId string, tlsReloader *cert.TlsReloader) { + externalCertPath := fmt.Sprintf("%s/%s/tls.cert", cfg.LndDir, cfg.ExternalSSLProvider) + err := os.Remove(externalCertPath) + if err != nil { + ltndLog.Warn("unable to delete temp or expiring cert at %v", externalCertPath) + } + + _, _, _, _, _, _, err = getTLSConfig(cfg, activeChainControl.KeyRing) + if err != nil { + err := fmt.Errorf("unable to load TLS credentials: %v", err) + ltndLog.Error(err) + // return err + } + + certBytes, keyBytes, err := cert.GetCertBytesFromPath(cfg.TLSCertPath, cfg.TLSKeyPath) + if err != nil { + // return err + } + + if cfg.TLSEncryptKey { + reader := bytes.NewReader(keyBytes) + keyBytes, err = lnencrypt.DecryptPayloadFromReader(reader, activeChainControl.KeyRing) + if err != nil { + ltndLog.Error(err) + // return err + } + } + + err = certprovider.RevokeCert(certId) + if err != nil { + ltndLog.Error("Failed to revoke temporary certifiate:") + } + + // Switch the server's TLS certificate to the new or persistent one. + err = tlsReloader.AttemptReload(certBytes, keyBytes) + if err != nil { + ltndLog.Error(err) + //return err + } } // fileExists reports whether the named file or directory exists.