-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial macaroon implementation #250
Conversation
@aakselrod, thanks for your PR! By analyzing the history of the files in this pull request, we identified @Roasbeef, @AndrewSamokhvalov and @bryanvu to be potential reviewers. |
1c29791
to
9bfa4ac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for coding it up. I am not yet up to speed so I cannot confidently ack but it looks like an excellent step forward.
cmd/lncli/main.go
Outdated
@@ -52,18 +64,22 @@ func getClientConn(ctx *cli.Context) *grpc.ClientConn { | |||
// * https://github.com/go-macaroon/macaroon | |||
cfg := config{ | |||
TLSCertPath: defaultTLSCertPath, | |||
AdmMacPath: defaultAdmMacPath, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(trivial) Consider expanding these spellings to AdminMacPath
and defaultAdminMacPath
throughout for clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now made this change.
cmd/lncli/main.go
Outdated
// the server clock is behind the client clock, or shortened by the time | ||
// the server clock is ahead of the client clock (or invalid altogether | ||
// if, in the latter case, this time is more than 60 seconds). | ||
timeCaveat := checkers.TimeBeforeCaveat(time.Now().Add(time.Minute)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this expiration be configurable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expiration is now configurable with --macaroontimeout
.
cmd/lncli/main.go
Outdated
// lets us try to get the highest possible permissions by default when | ||
// the command line doesn't specify a macaroon file to use for auth. | ||
macPath := cleanAndExpandPath(ctx.GlobalString("macaroonpath")) | ||
if !fileExists(macPath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the command line specifies a macaroon file, but that file doesn't exist due to a typo, could this admin fallback approach unintentionally escalate privilege?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken out all dependency on lnd.conf
, so only the command-line argument is used, which defaults to ~/.lnd/admin.macaroon
.
lnd.go
Outdated
} | ||
|
||
// Create macaroon files for lncli to use if they don't exist. | ||
if !fileExists(cfg.AdmMacPath) && !fileExists(cfg.ReadMacPath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if only one of them exists?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It assumes that this is intentional (specifically, you can remove admin.macaroon
and only have a read-only macaroon available, which will be really nice for autopilot to collect fees).
utACK but needs rebased |
It's already rebased. |
Oh, right. It was rebased. CI is just having troubles with simultaneous test runners, I guess.
|
Does this work here? (Talking to CI bot) test this please |
macaroons/auth.go
Outdated
|
||
// MacaroonSource wraps a macaroon to implement the | ||
// credentials.PerRPCCredentials interface. | ||
type MacaroonSource struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO something along the lines of MacaroonCredential
would be a more descriptive name for this struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea, I'll change this.
"time" | ||
|
||
"gopkg.in/macaroon-bakery.v1/bakery/checkers" | ||
"gopkg.in/macaroon.v1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't be using gopkg.in
here when importing the packages, and also when specifying the new dependancies in the glide.yaml
file. gopkg.in
was initially created in a time before vendoring support was added to Go. It's a way to create a repo that pins a project against a particular version. But since we use glide
for dependency management, it isn't needed IMO.
Instead we can pin directly against the two repositories directly:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, macaroon-bakery
actually imports itself, the macaroon package, and other libraries using gopkg.in
. See, for example, https://github.com/go-macaroon-bakery/macaroon-bakery/blob/v1/bakery/service.go#L14
We can fork these libraries or submit upstream PRs to remove the dependency on gopkg.in
, but I haven't been able to get it to work without that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like some feedback here: should I submit a PR upstream to fix this issue by helping the original author use glide? If so, should I modify lnd
's glide configuration to use my fork unless/until the upstream PR is merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, macaroon-bakery actually imports itself, the macaroon package, and other libraries using gopkg.in
Le sigh...
Alright, for now let's do this: we'll merge this as is now, but then we'll fork the repo's and modify them to not use gopkg.in
in a follow up PR.
cmd/lncli/main.go
Outdated
// altogether if, in the latter case, this time is more than 60 | ||
// seconds). | ||
// TODO(aakselrod): add better anti-replay protection. | ||
timeCaveat := checkers.TimeBeforeCaveat( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a suggested partial re-ordering in order to increase readability a bit:
macaroonTimeout := time.Duration(ctx.GlobalInt64("macaroontimeout"))
requestTimeout := time.Now().Add(time.Second * macaroonTimeout)
timeCaveat := checkers.TimeBeforeCaveat(requestTimeout)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will update these lines to improve readability.
lnd.go
Outdated
@@ -108,7 +136,7 @@ func lndMain() error { | |||
net.JoinHostPort("", strconv.Itoa(cfg.PeerPort)), | |||
} | |||
server, err := newServer(defaultListenAddrs, chanDB, activeChainControl, | |||
idPrivKey) | |||
idPrivKey, macaroonService) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only part of the way through this PR but, at first glance, it seems that the rpcServer
should be tracking the macaroonService
, rather than the server
itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes more sense. I'll change it.
} | ||
|
||
// Create macaroon files for lncli to use if they don't exist. | ||
if !fileExists(cfg.AdminMacPath) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the genMacaroons
function writes the admin macaroon, but then lnd
fails for some reason (crash, unplug, etc), we'll end up in a partial state that current isn't addressed. In this case, only the admin macaroon will have ben created meaning that the body of this conditional won't be executed.
In order to address this the &
should be changed to an ||
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original logic behind this was that you could delete the admin macaroon and leave a read-only one without an admin macaroon being regenerated. If you still prefer I change this to a ||
, I will do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like some feedback here: based on my comment above, should I change this to ||
or leave as &&
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, upon further reflection this makes sense!
macaroons/store.go
Outdated
// RootKey implements the RootKey method for the bakery.RootKeyStorage | ||
// interface. | ||
// TODO(aakselrod): Add support for key rotation. | ||
func (r *RootKeyStorage) RootKey() (rootKey []byte, id string, err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We typically avoid using named returns to reduction in readability they typically cause.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will fix this; I was using the same convention used by the interface being implemented.
macaroons/store.go
Outdated
if len(rootKey) == 0 { | ||
// Create a 24-byte root key to emulate the memory | ||
// storage that comes with bakery. | ||
rootKey = make([]byte, 24) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the value 24
included anywhere within the macaroon package as a constant? If so, then it should be used here in place of the magic number.
Also I'm wondering why the library opts to go with 24-byte
root keys rather than 32-byte
root keys...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value 24
isn't included in the package as a relevant constant as far as I can tell, and is specified as a literal in every call to randomBytes
. There's a NonceLen
constant that specifies 24 bytes, but it's not used for this purpose.
Increasing the root key size to 32 bytes shouldn't cause any problems; there is a semi-relevant constant called KeyLen
that specifies 32 bytes which is used for Ed25519 keys. I can use that constant if desired, or create a new constant, or just use a literal.
macaroons/store.go
Outdated
// Create a 24-byte root key to emulate the memory | ||
// storage that comes with bakery. | ||
rootKey = make([]byte, 24) | ||
length, err := rand.Read(rootKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the check to the number of bytes returned by the call to rand.Read
by instead using io.ReadFull
its place. This would look something like:
rootKey := make([]byte, 24)
if err := io.ReadFull(rand.Read, rootKey[:]); err != nil {
return err
}
return ns.Put([]byte(id), rootKey)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks more readable and I'll change the code to use it instead.
macaroons/store.go
Outdated
ns := tx.Bucket(rootKeyBucketName) | ||
rootKey = ns.Get([]byte(id)) | ||
// If there's no root key stored in the bucket yet, create one. | ||
if len(rootKey) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit of indentation can be avoided by returning early if len(rootKey) != 0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point; will change this.
rpcserver.go
Outdated
|
||
// roMethods is a slice of method names that are considered "read-only" | ||
// for authorization purposes, all lowercase. | ||
roMethods = []string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following methods I consider to be read-only are missing from this list:
SubscribeTransactions
SubscribeInvoices
SubscribeChannelGraph
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you recall our conversation, these subscribe
methods are now using the same permissions as list
or get
methods as they're accessing the same data. Perhaps renaming the list from roMethods
to something like roPermissions
would make that more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very happy to see the oldest open issue in the tracker finally being addressed!
This PR is a great start to the usage of macaroons as the primary authentication mechanism for lnd
's RPC interfaces. I'm very excited to explore more advanced functionality in the future beyond the current admin -> readOnly attenuation.
I've tested this locally over the past day or so on my local node and haven't ran into any major issues. None of my review comments are fundamental, so this should be able to be included in short order as I'd like this to land in our upcoming release.
macaroons/store.go
Outdated
ns := tx.Bucket(rootKeyBucketName) | ||
rootKey = ns.Get([]byte(id)) | ||
// If there's no root key stored in the bucket yet, create one. | ||
if len(rootKey) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point; will change this.
} | ||
|
||
// Create macaroon files for lncli to use if they don't exist. | ||
if !fileExists(cfg.AdminMacPath) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original logic behind this was that you could delete the admin macaroon and leave a read-only one without an admin macaroon being regenerated. If you still prefer I change this to a ||
, I will do so.
rpcserver.go
Outdated
// Check macaroon to see if this is allowed. | ||
if r.server.authSvc != nil { | ||
if err := macaroons.ValidateMacaroon(updateStream.Context(), | ||
"listinvoices", r.server.authSvc); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See here, for example, where SubscribeInvoices
actually uses a listinvoices
permission.
lnd.go
Outdated
@@ -108,7 +136,7 @@ func lndMain() error { | |||
net.JoinHostPort("", strconv.Itoa(cfg.PeerPort)), | |||
} | |||
server, err := newServer(defaultListenAddrs, chanDB, activeChainControl, | |||
idPrivKey) | |||
idPrivKey, macaroonService) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes more sense. I'll change it.
macaroons/store.go
Outdated
if len(rootKey) == 0 { | ||
// Create a 24-byte root key to emulate the memory | ||
// storage that comes with bakery. | ||
rootKey = make([]byte, 24) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value 24
isn't included in the package as a relevant constant as far as I can tell, and is specified as a literal in every call to randomBytes
. There's a NonceLen
constant that specifies 24 bytes, but it's not used for this purpose.
Increasing the root key size to 32 bytes shouldn't cause any problems; there is a semi-relevant constant called KeyLen
that specifies 32 bytes which is used for Ed25519 keys. I can use that constant if desired, or create a new constant, or just use a literal.
macaroons/store.go
Outdated
// Create a 24-byte root key to emulate the memory | ||
// storage that comes with bakery. | ||
rootKey = make([]byte, 24) | ||
length, err := rand.Read(rootKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks more readable and I'll change the code to use it instead.
rpcserver.go
Outdated
@@ -1522,6 +1686,13 @@ func (r *rpcServer) AddInvoice(ctx context.Context, | |||
// returned. | |||
func (r *rpcServer) LookupInvoice(ctx context.Context, | |||
req *lnrpc.PaymentHash) (*lnrpc.Invoice, error) { | |||
// Check macaroon to see if this is allowed. | |||
if r.server.authSvc != nil { | |||
if err := macaroons.ValidateMacaroon(ctx, "lookupinvoice", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the topic of using the same permission for the same data, should lookupinvoice
be the same permission as listinvoices
? The reason I chose not to do this is that lookupinvoice
requires a known payment hash and retrieves a single invoice, whereas listinvoices
allows the caller to retrieve all invoices indiscriminately and also to subscribe to invoice notifications.
"time" | ||
|
||
"gopkg.in/macaroon-bakery.v1/bakery/checkers" | ||
"gopkg.in/macaroon.v1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like some feedback here: should I submit a PR upstream to fix this issue by helping the original author use glide? If so, should I modify lnd
's glide configuration to use my fork unless/until the upstream PR is merged?
} | ||
|
||
// Create macaroon files for lncli to use if they don't exist. | ||
if !fileExists(cfg.AdminMacPath) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like some feedback here: based on my comment above, should I change this to ||
or leave as &&
?
rpcserver.go
Outdated
"channelbalance", | ||
"pendingchannels", | ||
"listchannels", | ||
"lookupinvoice", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would like some feedback here: should I merge the lookupinvoice
and listinvoices
permissions? What about the listchannels
and pendingchannels
permissions? Any others that I'm missing that might be good to merge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, yeah merging them makes sense as they query the same data in the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🍪
This is an initial macaroon implementation. It partially fixes issue #20.
lncli
already includes the client-side macaroon usage with basic replay protection (an additional caveat that makes the request good for only 1 minute by default). To authenticate your request tolnd
with your own framework, put the hex-encoded macaroon in the GRPC request's metadata under the keymacaroon
. To authenticate your request tolnd
via the REST proxy, put the hex-encoded macaroon in an HTTP header calledGrpc-metadata-macaroon
like so:Macaroons can be turned off with the
--no-macaroons
option to bothlnd
andlncli
. Iflnd
is run with--no-macaroons
and the client passes a macaroon, the macaroon will be ignored. Iflnd
is run with--no-macaroons
before it generates macaroon files,lncli
must be run with--no-macaroons
or it will still look for a macaroon file and fail.This PR also removes
lncli
's dependency onlnd.conf
for the TLS certificate, as that was causing issues for users. Now, the TLS certificate specification behaves like macaroon file specification: only a command-line switch with a default settings.Additional future improvements over the next weeks/months: