walletunlocker+lnd: implement stateless wallet initialization

This commit is contained in:
Oliver Gugger committed 2020-11-07 11:24:34 +01:00
1 parent 4b0b20c98b
commit e7aa9256ab
3 files changed
+566 -94

No files matched your search

+113 -44
View File
@@ -410,7 +410,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
if !cfg.NoMacaroons {
// Create the macaroon authentication/authorization service.
macaroonService, err = macaroons.NewService(
cfg.networkDir, "lnd", false, macaroons.IPLockChecker,
cfg.networkDir, "lnd", walletInitParams.StatelessInit,
macaroons.IPLockChecker,
)
if err != nil {
err := fmt.Errorf("unable to set up macaroon "+
@@ -421,17 +422,42 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
defer macaroonService.Close()
// Try to unlock the macaroon store with the private password.
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
// wallet unlocker.
err = macaroonService.CreateUnlock(&privateWalletPw)
if err != nil {
if err != nil && err != macaroons.ErrAlreadyUnlocked {
err := fmt.Errorf("unable to unlock macaroons: %v", err)
ltndLog.Error(err)
return err
}
// Create macaroon files for lncli to use if they don't exist.
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) &&
// In case we actually needed to unlock the wallet, we now need
// to create an instance of the admin macaroon and send it to
// the unlocker so it can forward it to the user. In no seed
// backup mode, there's nobody listening on the channel and we'd
// block here forever.
if !cfg.NoSeedBackup {
adminMacBytes, err := bakeMacaroon(
ctx, macaroonService, adminPermissions(),
)
if err != nil {
return err
}
// The channel is buffered by one element so writing
// should not block here.
walletInitParams.MacResponseChan <- adminMacBytes
}
// If the user requested a stateless initialization, no macaroon
// files should be created.
if !walletInitParams.StatelessInit &&
!fileExists(cfg.AdminMacPath) &&
!fileExists(cfg.ReadMacPath) &&
!fileExists(cfg.InvoiceMacPath) {
// Create macaroon files for lncli to use if they don't
// exist.
err = genMacaroons(
ctx, macaroonService, cfg.AdminMacPath,
cfg.ReadMacPath, cfg.InvoiceMacPath,
@@ -443,6 +469,28 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
return err
}
}
// As a security service to the user, if they requested
// stateless initialization and there are macaroon files on disk
// we log a warning.
if walletInitParams.StatelessInit {
msg := "Found %s macaroon on disk (%s) even though " +
"--stateless_init was requested. Unencrypted " +
"state is accessible by the host system. You " +
"should change the password and use " +
"--new_mac_root_key with --stateless_init to " +
"clean up and invalidate old macaroons."
if fileExists(cfg.AdminMacPath) {
ltndLog.Warnf(msg, "admin", cfg.AdminMacPath)
}
if fileExists(cfg.ReadMacPath) {
ltndLog.Warnf(msg, "readonly", cfg.ReadMacPath)
}
if fileExists(cfg.InvoiceMacPath) {
ltndLog.Warnf(msg, "invoice", cfg.InvoiceMacPath)
}
}
}
// Now we're definitely done with the unlocker, shut it down so we can
@@ -950,6 +998,21 @@ func fileExists(name string) bool {
return true
}
// bakeMacaroon creates a new macaroon with newest version and the given
// permissions then returns it binary serialized.
func bakeMacaroon(ctx context.Context, svc *macaroons.Service,
permissions []bakery.Op) ([]byte, error) {
mac, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, permissions...,
)
if err != nil {
return nil, err
}
return mac.M().MarshalBinary()
}
// genMacaroons generates three macaroon files; one admin-level, one for
// invoice access and one read-only. These can also be used to generate more
// granular macaroons.
@@ -960,57 +1023,48 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
// access invoice related calls. This is useful for merchants and other
// services to allow an isolated instance that can only query and
// modify invoices.
invoiceMac, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, invoicePermissions...,
)
if err != nil {
return err
}
invoiceMacBytes, err := invoiceMac.M().MarshalBinary()
invoiceMacBytes, err := bakeMacaroon(ctx, svc, invoicePermissions)
if err != nil {
return err
}
err = ioutil.WriteFile(invoiceFile, invoiceMacBytes, 0644)
if err != nil {
os.Remove(invoiceFile)
_ = os.Remove(invoiceFile)
return err
}
// Generate the read-only macaroon and write it to a file.
roMacaroon, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, readPermissions...,
)
if err != nil {
return err
}
roBytes, err := roMacaroon.M().MarshalBinary()
roBytes, err := bakeMacaroon(ctx, svc, readPermissions)
if err != nil {
return err
}
if err = ioutil.WriteFile(roFile, roBytes, 0644); err != nil {
os.Remove(admFile)
_ = os.Remove(roFile)
return err
}
// Generate the admin macaroon and write it to a file.
adminPermissions := append(readPermissions, writePermissions...)
admMacaroon, err := svc.NewMacaroon(
ctx, macaroons.DefaultRootKeyID, adminPermissions...,
)
if err != nil {
return err
}
admBytes, err := admMacaroon.M().MarshalBinary()
admBytes, err := bakeMacaroon(ctx, svc, adminPermissions())
if err != nil {
return err
}
if err = ioutil.WriteFile(admFile, admBytes, 0600); err != nil {
_ = os.Remove(admFile)
return err
}
return nil
}
// adminPermissions returns a list of all permissions in a safe way that doesn't
// modify any of the source lists.
func adminPermissions() []bakery.Op {
admin := make([]bakery.Op, len(readPermissions)+len(writePermissions))
copy(admin[:len(readPermissions)], readPermissions)
copy(admin[len(readPermissions):], writePermissions)
return admin
}
// WalletUnlockParams holds the variables used to parameterize the unlocking of
// lnd's wallet after it has already been created.
type WalletUnlockParams struct {
@@ -1039,6 +1093,15 @@ type WalletUnlockParams struct {
// UnloadWallet is a function for unloading the wallet, which should
// be called on shutdown.
UnloadWallet func() error
// StatelessInit signals that the user requested the daemon to be
// initialized stateless, which means no unencrypted macaroons should be
// written to disk.
StatelessInit bool
// MacResponseChan is the channel for sending back the admin macaroon to
// the WalletUnlocker service.
MacResponseChan chan []byte
}
// waitForWalletPassword will spin up gRPC and REST endpoints for the
@@ -1054,12 +1117,10 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
chainConfig = cfg.Litecoin
}
// The macaroon files are passed to the wallet unlocker since they are
// also encrypted with the wallet's password. These files will be
// deleted within it and recreated when successfully changing the
// wallet's password.
// The macaroonFiles are passed to the wallet unlocker so they can be
// deleted and recreated in case the root macaroon key is also changed
// during the change password operation.
macaroonFiles := []string{
filepath.Join(cfg.networkDir, macaroons.DBFilename),
cfg.AdminMacPath, cfg.ReadMacPath, cfg.InvoiceMacPath,
}
pwService := walletunlocker.New(
@@ -1074,6 +1135,10 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
var shutdownFuncs []func()
shutdown := func() {
// Make sure nothing blocks on reading on the macaroon channel,
// otherwise the GracefulStop below will never return.
close(pwService.MacResponseChan)
for _, shutdownFn := range shutdownFuncs {
shutdownFn()
}
@@ -1204,23 +1269,27 @@ func waitForWalletPassword(cfg *Config, restEndpoints []net.Addr,
}
return &WalletUnlockParams{
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
Wallet: newWallet,
ChansToRestore: initMsg.ChanBackups,
UnloadWallet: loader.UnloadWallet,
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
Wallet: newWallet,
ChansToRestore: initMsg.ChanBackups,
UnloadWallet: loader.UnloadWallet,
StatelessInit: initMsg.StatelessInit,
MacResponseChan: pwService.MacResponseChan,
}, shutdown, nil
// The wallet has already been created in the past, and is simply being
// unlocked. So we'll just return these passphrases.
case unlockMsg := <-pwService.UnlockMsgs:
return &WalletUnlockParams{
Password: unlockMsg.Passphrase,
RecoveryWindow: unlockMsg.RecoveryWindow,
Wallet: unlockMsg.Wallet,
ChansToRestore: unlockMsg.ChanBackups,
UnloadWallet: unlockMsg.UnloadWallet,
Password: unlockMsg.Passphrase,
RecoveryWindow: unlockMsg.RecoveryWindow,
Wallet: unlockMsg.Wallet,
ChansToRestore: unlockMsg.ChanBackups,
UnloadWallet: unlockMsg.UnloadWallet,
StatelessInit: unlockMsg.StatelessInit,
MacResponseChan: pwService.MacResponseChan,
}, shutdown, nil
case <-signal.ShutdownChannel():