Merge pull request #12 from wpaulino/lsat-mint

multi: introduce proper LSAT creation and verification
This commit is contained in:
Oliver Gugger
2019-11-28 14:25:46 +01:00
committed by GitHub
20 changed files with 1178 additions and 278 deletions

View File

@@ -1,15 +1,17 @@
package auth
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net/http"
"regexp"
"github.com/lightninglabs/kirin/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
"gopkg.in/macaroon.v2"
)
@@ -36,8 +38,7 @@ var (
// LsatAuthenticator is an authenticator that uses the LSAT protocol to
// authenticate requests.
type LsatAuthenticator struct {
challenger Challenger
macService *macaroons.Service
minter Minter
}
// A compile time flag to ensure the LsatAuthenticator satisfies the
@@ -46,44 +47,35 @@ var _ Authenticator = (*LsatAuthenticator)(nil)
// NewLsatAuthenticator creates a new authenticator that authenticates requests
// based on LSAT tokens.
func NewLsatAuthenticator(challenger Challenger) (*LsatAuthenticator, error) {
macService, err := macaroons.NewService()
if err != nil {
return nil, err
}
return &LsatAuthenticator{
challenger: challenger,
macService: macService,
}, nil
func NewLsatAuthenticator(minter Minter) *LsatAuthenticator {
return &LsatAuthenticator{minter: minter}
}
// Accept returns whether or not the header successfully authenticates the user
// to a given backend service.
//
// NOTE: This is part of the Authenticator interface.
func (l *LsatAuthenticator) Accept(header *http.Header) bool {
func (l *LsatAuthenticator) Accept(header *http.Header, serviceName string) bool {
// Try reading the macaroon and preimage from the HTTP header. This can
// be in different header fields depending on the implementation and/or
// protocol.
mac, preimageBytes, err := FromHeader(header)
mac, preimage, err := FromHeader(header)
if err != nil {
log.Debugf("Deny: %v", err)
return false
}
// TODO(guggero): check preimage against payment hash caveat in the
// macaroon.
if len(preimageBytes) != 32 {
log.Debugf("Deny: Decoded preimage has invalid length.")
verificationParams := &mint.VerificationParams{
Macaroon: mac,
Preimage: preimage,
TargetService: serviceName,
}
err = l.minter.VerifyLSAT(context.Background(), verificationParams)
if err != nil {
log.Debugf("Deny: LSAT validation failed: %v", err)
return false
}
err = l.macService.ValidateMacaroon(mac, []bakery.Op{})
if err != nil {
log.Debugf("Deny: Macaroon validation failed: %v", err)
return false
}
return true
}
@@ -91,34 +83,24 @@ func (l *LsatAuthenticator) Accept(header *http.Header) bool {
// complete.
//
// NOTE: This is part of the Authenticator interface.
func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) (
http.Header, error) {
func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request,
serviceName string) (http.Header, error) {
paymentRequest, paymentHash, err := l.challenger.NewChallenge()
if err != nil {
log.Errorf("Error creating new challenge: %v", err)
return nil, err
}
// Create a new macaroon and add the payment hash as a caveat.
// The bakery requires at least one operation so we add an "allow all"
// permission set for now.
mac, err := l.macService.NewMacaroon(
[]bakery.Op{{Entity: opWildcard, Action: opWildcard}}, []string{
checkers.Condition(
macaroons.CondRHash, paymentHash.String(),
),
},
service := lsat.Service{Name: serviceName, Tier: lsat.BaseTier}
mac, paymentRequest, err := l.minter.MintLSAT(
context.Background(), service,
)
if err != nil {
log.Errorf("Error creating macaroon: %v", err)
log.Errorf("Error minting LSAT: %v", err)
return nil, err
}
macBytes, err := mac.MarshalBinary()
if err != nil {
log.Errorf("Error serializing LSAT: %v", err)
}
str := "LSAT macaroon='%s' invoice='%s'"
str = fmt.Sprintf(
str, base64.StdEncoding.EncodeToString(mac), paymentRequest,
)
str := fmt.Sprintf("LSAT macaroon='%s' invoice='%s'",
base64.StdEncoding.EncodeToString(macBytes), paymentRequest)
header := r.Header
header.Set("WWW-Authenticate", str)
@@ -134,7 +116,7 @@ func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) (
// 3. Macaroon: <macHex>
// If only the macaroon is sent in header 2 or three then it is expected to have
// a caveat with the preimage attached to it.
func FromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) {
func FromHeader(header *http.Header) (*macaroon.Macaroon, lntypes.Preimage, error) {
var authHeader string
switch {
@@ -147,37 +129,37 @@ func FromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) {
log.Debugf("Trying to authorize with header value [%s].",
authHeader)
if !authRegex.MatchString(authHeader) {
return nil, nil, fmt.Errorf("invalid auth header "+
"format: %s", authHeader)
return nil, lntypes.Preimage{}, fmt.Errorf("invalid "+
"auth header format: %s", authHeader)
}
matches := authRegex.FindStringSubmatch(authHeader)
if len(matches) != 3 {
return nil, nil, fmt.Errorf("invalid auth header "+
"format: %s", authHeader)
return nil, lntypes.Preimage{}, fmt.Errorf("invalid "+
"auth header format: %s", authHeader)
}
// Decode the content of the two parts of the header value.
macBase64, preimageHex := matches[1], matches[2]
macBytes, err := base64.StdEncoding.DecodeString(macBase64)
if err != nil {
return nil, nil, fmt.Errorf("base64 decode of "+
"macaroon failed: %v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("base64 "+
"decode of macaroon failed: %v", err)
}
mac := &macaroon.Macaroon{}
err = mac.UnmarshalBinary(macBytes)
if err != nil {
return nil, nil, fmt.Errorf("unable to unmarshal "+
"macaroon: %v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("unable to "+
"unmarshal macaroon: %v", err)
}
preimageBytes, err := hex.DecodeString(preimageHex)
preimage, err := lntypes.MakePreimageFromStr(preimageHex)
if err != nil {
return nil, nil, fmt.Errorf("hex decode of preimage "+
"failed: %v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("hex "+
"decode of preimage failed: %v", err)
}
// All done, we don't need to extract anything from the
// macaroon since the preimage was presented separately.
return mac, preimageBytes, nil
return mac, preimage, nil
// Header field 2: Contains only the macaroon.
case header.Get(HeaderMacaroonMD) != "":
@@ -188,40 +170,41 @@ func FromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) {
authHeader = header.Get(HeaderMacaroon)
default:
return nil, nil, fmt.Errorf("no auth header provided")
return nil, lntypes.Preimage{}, fmt.Errorf("no auth header " +
"provided")
}
// For case 2 and 3, we need to actually unmarshal the macaroon to
// extract the preimage.
macBytes, err := hex.DecodeString(authHeader)
if err != nil {
return nil, nil, fmt.Errorf("hex decode of macaroon "+
"failed: %v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("hex decode of "+
"macaroon failed: %v", err)
}
mac := &macaroon.Macaroon{}
err = mac.UnmarshalBinary(macBytes)
if err != nil {
return nil, nil, fmt.Errorf("unable to unmarshal macaroon: "+
"%v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("unable to "+
"unmarshal macaroon: %v", err)
}
preimageHex, err := macaroons.ExtractCaveat(mac, macaroons.CondPreimage)
if err != nil {
return nil, nil, fmt.Errorf("unable to extract preimage from "+
"macaroon: %v", err)
preimageHex, ok := lsat.HasCaveat(mac, lsat.PreimageKey)
if !ok {
return nil, lntypes.Preimage{}, errors.New("preimage caveat " +
"not found")
}
preimageBytes, err := hex.DecodeString(preimageHex)
preimage, err := lntypes.MakePreimageFromStr(preimageHex)
if err != nil {
return nil, nil, fmt.Errorf("hex decode of preimage "+
"failed: %v", err)
return nil, lntypes.Preimage{}, fmt.Errorf("hex decode of "+
"preimage failed: %v", err)
}
return mac, preimageBytes, nil
return mac, preimage, nil
}
// SetHeader sets the provided authentication elements as the default/standard
// HTTP header for the LSAT protocol.
func SetHeader(header *http.Header, mac *macaroon.Macaroon,
preimage []byte) error {
preimage lntypes.Preimage) error {
macBytes, err := mac.MarshalBinary()
if err != nil {
@@ -229,7 +212,7 @@ func SetHeader(header *http.Header, mac *macaroon.Macaroon,
}
value := fmt.Sprintf(
authFormat, base64.StdEncoding.EncodeToString(macBytes),
hex.EncodeToString(preimage),
preimage.String(),
)
header.Set(HeaderAuthorization, value)
return nil

View File

@@ -7,7 +7,7 @@ import (
"testing"
"github.com/lightninglabs/kirin/auth"
"github.com/lightninglabs/kirin/macaroons"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
"gopkg.in/macaroon.v2"
)
@@ -27,9 +27,8 @@ func createDummyMacHex(preimage string) string {
if err != nil {
panic(err)
}
err = dummyMac.AddFirstPartyCaveat(
[]byte(macaroons.CondPreimage + " " + preimage),
)
preimageCaveat := lsat.Caveat{Condition: lsat.PreimageKey, Value: preimage}
err = lsat.AddFirstPartyCaveats(dummyMac, preimageCaveat)
if err != nil {
panic(err)
}
@@ -135,13 +134,9 @@ func TestLsatAuthenticator(t *testing.T) {
}
)
a, err := auth.NewLsatAuthenticator(&mockChallenger{})
if err != nil {
t.Fatalf("Could not create authenticator: %v", err)
}
a := auth.NewLsatAuthenticator(&mockMint{})
for _, testCase := range headerTests {
result := a.Accept(testCase.header)
result := a.Accept(testCase.header, "test")
if result != testCase.result {
t.Fatalf("test case %s failed. got %v expected %v",
testCase.id, result, testCase.result)

View File

@@ -1,9 +1,12 @@
package auth
import (
"context"
"net/http"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lsat"
"gopkg.in/macaroon.v2"
)
// Authenticator is the generic interface for validating client headers and
@@ -11,16 +14,19 @@ import (
type Authenticator interface {
// Accept returns whether or not the header successfully authenticates
// the user to a given backend service.
Accept(*http.Header) bool
Accept(*http.Header, string) bool
// FreshChallengeHeader returns a header containing a challenge for the
// user to complete.
FreshChallengeHeader(r *http.Request) (http.Header, error)
FreshChallengeHeader(*http.Request, string) (http.Header, error)
}
// Challenger is an interface for generating new payment challenges.
type Challenger interface {
// NewChallenge creates a new LSAT payment challenge, returning a
// payment request (invoice) and the corresponding payment hash.
NewChallenge() (string, lntypes.Hash, error)
// Minter is an entity that is able to mint and verify LSATs for a set of
// services.
type Minter interface {
// MintLSAT mints a new LSAT for the target services.
MintLSAT(context.Context, ...lsat.Service) (*macaroon.Macaroon, string, error)
// VerifyLSAT attempts to verify an LSAT with the given parameters.
VerifyLSAT(context.Context, *mint.VerificationParams) error
}

View File

@@ -5,6 +5,10 @@ import "net/http"
// MockAuthenticator is a mock implementation of the authenticator.
type MockAuthenticator struct{}
// A compile-time constraint to ensure MockAuthenticator implements
// Authenticator.
var _ Authenticator = (*MockAuthenticator)(nil)
// NewMockAuthenticator returns a new MockAuthenticator instance.
func NewMockAuthenticator() *MockAuthenticator {
return &MockAuthenticator{}
@@ -12,7 +16,7 @@ func NewMockAuthenticator() *MockAuthenticator {
// Accept returns whether or not the header successfully authenticates the user
// to a given backend service.
func (a MockAuthenticator) Accept(header *http.Header) bool {
func (a MockAuthenticator) Accept(header *http.Header, _ string) bool {
if header.Get("Authorization") != "" {
return true
}
@@ -27,7 +31,9 @@ func (a MockAuthenticator) Accept(header *http.Header) bool {
// FreshChallengeHeader returns a header containing a challenge for the user to
// complete.
func (a MockAuthenticator) FreshChallengeHeader(r *http.Request) (http.Header, error) {
func (a MockAuthenticator) FreshChallengeHeader(r *http.Request,
_ string) (http.Header, error) {
header := r.Header
header.Set("WWW-Authenticate", "LSAT macaroon='AGIAJEemVQUTEyNCR0exk7ek90Cg==' invoice='lnbc1500n1pw5kjhmpp5fu6xhthlt2vucmzkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptksct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzpgxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02elhypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadqy3ngqjcym5a'")
return header, nil

25
auth/mock_test.go Normal file
View File

@@ -0,0 +1,25 @@
package auth_test
import (
"context"
"github.com/lightninglabs/kirin/auth"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lsat"
"gopkg.in/macaroon.v2"
)
type mockMint struct {
}
var _ auth.Minter = (*mockMint)(nil)
func (m *mockMint) MintLSAT(_ context.Context,
services ...lsat.Service) (*macaroon.Macaroon, string, error) {
return nil, "", nil
}
func (m *mockMint) VerifyLSAT(_ context.Context, p *mint.VerificationParams) error {
return nil
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/lightninglabs/kirin/auth"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
@@ -22,13 +22,13 @@ type LndChallenger struct {
}
// A compile time flag to ensure the LndChallenger satisfies the
// Challenger interface.
var _ auth.Challenger = (*LndChallenger)(nil)
// mint.Challenger interface.
var _ mint.Challenger = (*LndChallenger)(nil)
// NewLndChallenger creates a new challenger that uses the given connection
// details to connect to an lnd backend to create payment challenges.
func NewLndChallenger(cfg *authConfig, genInvoiceReq InvoiceRequestGenerator) (
auth.Challenger, error) {
*LndChallenger, error) {
if genInvoiceReq == nil {
return nil, fmt.Errorf("genInvoiceReq cannot be nil")

View File

@@ -16,6 +16,12 @@ var (
defaultMaxLogFileSize = 10
)
type etcdConfig struct {
Host string `long:"host" description:"host:port of an active etcd instance"`
User string `long:"user" description:"user authorized to access the etcd host"`
Password string `long:"password" description:"password of the etcd user"`
}
type authConfig struct {
// LndHost is the hostname of the LND instance to connect to.
LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"`
@@ -36,6 +42,8 @@ type config struct {
// is located.
StaticRoot string `long:"staticroot" description:"The folder where the static content is located."`
Etcd *etcdConfig `long:"etcd" description:"Configuration for the etcd instance backing the proxy."`
Authenticator *authConfig `long:"authenticator" description:"Configuration for the authenticator."`
// Services is a list of JSON objects in string format, which specify

23
go.mod
View File

@@ -5,15 +5,32 @@ go 1.13
require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/coreos/etcd v3.3.17+incompatible
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 // indirect
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
github.com/golang/protobuf v1.3.2
github.com/lightninglabs/loop v0.2.4-alpha.0.20191116024025-539d6ed9e3e8
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191119135609-79051ac63f1a
github.com/google/btree v1.0.0 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/websocket v1.4.1 // indirect
github.com/jonboulle/clockwork v0.1.0 // indirect
github.com/json-iterator/go v1.1.8 // indirect
github.com/lightninglabs/loop v0.3.0-alpha.0.20191126005026-8b8b87844035
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191115230031-4d7a151b4763
github.com/lightningnetwork/lnd/cert v1.0.0
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/soheilhy/cmux v0.1.4 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
go.uber.org/zap v1.13.0 // indirect
golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea // indirect
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a // indirect
google.golang.org/grpc v1.25.1
gopkg.in/macaroon-bakery.v2 v2.1.0
gopkg.in/macaroon-bakery.v2 v2.1.0 // indirect
gopkg.in/macaroon.v2 v2.1.0
gopkg.in/yaml.v2 v2.2.2
sigs.k8s.io/yaml v1.1.0 // indirect
)

78
go.sum
View File

@@ -1,6 +1,7 @@
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 h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
@@ -61,9 +62,20 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.17+incompatible h1:f/Z3EoDSx1yjaIjLQGo1diYUlQYSBrrAQ5vP8NjwXwo=
github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 h1:FE783w8WFh+Rvg+7bZ5g8p7gP4SeVS4AoNwkvazlsBg=
github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
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/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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -84,16 +96,26 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/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 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
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.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
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 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
@@ -110,8 +132,12 @@ github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH04
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
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.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc=
@@ -131,6 +157,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
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=
@@ -140,15 +167,14 @@ 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/loop v0.2.4-alpha.0.20191116024025-539d6ed9e3e8 h1:ZH3Qd9f5jBoNdWDP9jkzBdsuKXwFB0fkEEnax3hPt3Q=
github.com/lightninglabs/loop v0.2.4-alpha.0.20191116024025-539d6ed9e3e8/go.mod h1:9tvOOyUhd3AcfrxLz/dJSTHJ0ouqA+u6utJ+fBYrk9M=
github.com/lightninglabs/loop v0.3.0-alpha.0.20191126005026-8b8b87844035 h1:Sw8Ibk3xIZgneHvmAcob1uVz55DYn8mvvjbcpH9CH0w=
github.com/lightninglabs/loop v0.3.0-alpha.0.20191126005026-8b8b87844035/go.mod h1:9tvOOyUhd3AcfrxLz/dJSTHJ0ouqA+u6utJ+fBYrk9M=
github.com/lightninglabs/neutrino v0.11.0 h1:lPpYFCtsfJX2W5zI4pWycPmbbBdr7zU+BafYdLoD6k0=
github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg=
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a h1:GoWPN4i4jTKRxhVNh9a2vvBBO1Y2seiJB+SopUYoKyo=
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191115230031-4d7a151b4763 h1:Eit9hH737th2WwnnVhkuxyMqgXce6keztGTtbDMLJ80=
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191115230031-4d7a151b4763/go.mod h1:Z7DDVIgvMgyb/4+btLeiU++xt49T35PNunXGCvAaxiE=
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191119135609-79051ac63f1a h1:0N5HflbWT8QjrFh9UZ8qJj6VU86YDeCJ4LtHCetMFUM=
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191119135609-79051ac63f1a/go.mod h1:51tWqgjX5ZfYOLTlgkn7fQXCODJPUD3d1t1CsV1DKS4=
github.com/lightningnetwork/lnd/cert v1.0.0 h1:J0gtf2UNQX2U+/j5cXnX2wIMSTuJuwrXv7m9qJr2wtw=
github.com/lightningnetwork/lnd/cert v1.0.0/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo=
github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0=
@@ -162,6 +188,11 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
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=
@@ -171,6 +202,9 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
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 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
@@ -178,6 +212,7 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
@@ -189,21 +224,42 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
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 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
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/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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
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 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
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/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/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-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -211,6 +267,9 @@ golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTk
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 h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
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=
@@ -222,6 +281,7 @@ golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73r
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-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI=
@@ -255,6 +315,11 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
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 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
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=
@@ -276,6 +341,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
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=
@@ -296,3 +362,7 @@ gopkg.in/yaml.v2 v2.2.2/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 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@@ -6,8 +6,11 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/lightninglabs/kirin/auth"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/kirin/proxy"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/cert"
@@ -15,6 +18,16 @@ import (
"gopkg.in/yaml.v2"
)
const (
// topLevelKey is the top level key for an etcd cluster where we'll
// store all LSAT proxy related data.
topLevelKey = "lsat/proxy"
// etcdKeyDelimeter is the delimeter we'll use for all etcd keys to
// represent a path-like structure.
etcdKeyDelimeter = "/"
)
// Main is the true entrypoint of Kirin.
func Main() {
// TODO: Prevent from running twice.
@@ -39,6 +52,17 @@ func start() error {
return fmt.Errorf("unable to set up logging: %v", err)
}
// Initialize our etcd client.
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: []string{cfg.Etcd.Host},
DialTimeout: 5 * time.Second,
Username: cfg.Etcd.User,
Password: cfg.Etcd.Password,
})
if err != nil {
return fmt.Errorf("unable to connect to etcd: %v", err)
}
// Create the proxy and connect it to lnd.
genInvoiceReq := func() (*lnrpc.Invoice, error) {
return &lnrpc.Invoice{
@@ -46,7 +70,10 @@ func start() error {
Value: 1,
}, nil
}
servicesProxy, err := createProxy(cfg, genInvoiceReq)
servicesProxy, err := createProxy(cfg, genInvoiceReq, etcdClient)
if err != nil {
return err
}
server := &http.Server{
Addr: cfg.ListenAddr,
Handler: http.HandlerFunc(servicesProxy.ServeHTTP),
@@ -70,7 +97,7 @@ func start() error {
// The ListenAndServeTLS below will block until shut down or an error
// occurs. So we can just defer a cleanup function here that will close
// everything on shutdown.
defer cleanup(server)
defer cleanup(etcdClient, server)
// Finally start the server.
log.Infof("Starting the server, listening on %s.", cfg.ListenAddr)
@@ -127,24 +154,27 @@ func setupLogging(cfg *config) error {
}
// createProxy creates the proxy with all the services it needs.
func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator) (
*proxy.Proxy, error) {
func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator,
etcdClient *clientv3.Client) (*proxy.Proxy, error) {
challenger, err := NewLndChallenger(
cfg.Authenticator, genInvoiceReq,
)
if err != nil {
return nil, err
}
authenticator, err := auth.NewLsatAuthenticator(challenger)
challenger, err := NewLndChallenger(cfg.Authenticator, genInvoiceReq)
if err != nil {
return nil, err
}
minter := mint.New(&mint.Config{
Challenger: challenger,
Secrets: newSecretStore(etcdClient),
ServiceLimiter: newStaticServiceLimiter(cfg.Services),
})
authenticator := auth.NewLsatAuthenticator(minter)
return proxy.New(authenticator, cfg.Services, cfg.StaticRoot)
}
// cleanup closes the given server and shuts down the log rotator.
func cleanup(server *http.Server) {
func cleanup(etcdClient *clientv3.Client, server *http.Server) {
if err := etcdClient.Close(); err != nil {
log.Errorf("Error terminating etcd client: %v", err)
}
err := server.Close()
if err != nil {
log.Errorf("Error closing server: %v", err)

View File

@@ -1,151 +0,0 @@
package macaroons
import (
"context"
"encoding/hex"
"fmt"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
"gopkg.in/macaroon.v2"
)
const (
// CondRHash is the macaroon caveat condition for a payment hash.
CondRHash = "r-hash"
// CondPreimage is the macaroon caveat condition for a payment preimage.
CondPreimage = "preimage"
)
var (
rootKey = "aabbccddeeff00112233445566778899"
rootKeyId = []byte("0")
)
type rootKeyStore struct{}
// A compile time flag to ensure the rootKeyStore satisfies the
// bakery.RootKeyStore interface.
var _ bakery.RootKeyStore = (*rootKeyStore)(nil)
func (r *rootKeyStore) Get(_ context.Context, id []byte) ([]byte, error) {
return hex.DecodeString(rootKey)
}
func (r *rootKeyStore) RootKey(_ context.Context) (rootKey, id []byte,
err error) {
key, err := r.Get(nil, rootKeyId)
if err != nil {
return nil, nil, err
}
return key, rootKeyId, nil
}
// Service can bake and validate macaroons.
type Service struct {
bakery.Bakery
}
// NewService creates a new macaroon service with the given checker functions
// that should be supported when validating a macaroon.
func NewService(checks ...macaroons.Checker) (*Service, error) {
macaroonParams := bakery.BakeryParams{
Location: "kirin",
RootKeyStore: &rootKeyStore{},
Locator: nil,
Key: nil,
}
svc := bakery.New(macaroonParams)
// Register all custom caveat checkers with the bakery's checker.
checker := svc.Checker.FirstPartyCaveatChecker.(*checkers.Checker)
for _, check := range checks {
cond, fun := check()
if !isRegistered(checker, cond) {
checker.Register(cond, "std", fun)
}
}
return &Service{*svc}, nil
}
// NewMacaroon bakes a new macaroon with the given allowed operations and
// optional first-party caveats.
func (s *Service) NewMacaroon(operations []bakery.Op, caveats []string) (
[]byte, error) {
ctx := context.Background()
mac, err := s.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, operations...,
)
if err != nil {
return nil, err
}
// Add all first party caveats before serializing the macaroon.
for _, caveat := range caveats {
err := mac.M().AddFirstPartyCaveat([]byte(caveat))
if err != nil {
return nil, err
}
}
macBytes, err := mac.M().MarshalBinary()
if err != nil {
return nil, err
}
return macBytes, nil
}
// ValidateMacaroon verifies the signature chain of a macaroon and then
// checks that none of the applied restrictions are violated.
func (s *Service) ValidateMacaroon(mac *macaroon.Macaroon,
perms []bakery.Op) error {
// Check the method being called against the permitted operation and
// the expiration time and IP address and return the result.
authChecker := s.Checker.Auth(macaroon.Slice{mac})
_, err := authChecker.Allow(context.Background(), perms...)
return err
}
// ExtractCaveat extracts the value of a given caveat condition or returns an
// empty string if that caveat does not exist.
func ExtractCaveat(mac *macaroon.Macaroon, cond string) (string, error) {
if mac == nil {
return "", fmt.Errorf("macaroon cannot be nil")
}
for _, caveat := range mac.Caveats() {
cavStr := string(caveat.Id)
cavCond, cavArg, err := checkers.ParseCaveat(cavStr)
if err != nil {
continue
}
if cavCond == cond {
return cavArg, nil
}
}
return "", nil
}
// isRegistered checks to see if the required checker has already been
// registered in order to avoid a panic caused by double registration.
func isRegistered(c *checkers.Checker, name string) bool {
if c == nil {
return false
}
for _, info := range c.Info() {
if info.Name == name &&
info.Prefix == "" &&
info.Namespace == "std" {
return true
}
}
return false
}

256
mint/mint.go Normal file
View File

@@ -0,0 +1,256 @@
package mint
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
"gopkg.in/macaroon.v2"
)
var (
// ErrSecretNotFound is an error returned when we attempt to retrieve a
// secret by its key but it is not found.
ErrSecretNotFound = errors.New("secret not found")
)
// Challenger is an interface used to present requesters of LSATs with a
// challenge that must be satisfied before an LSAT can be validated. This
// challenge takes the form of a Lightning payment request.
type Challenger interface {
// NewChallenge returns a new challenge in the form of a Lightning
// payment request. The payment hash is also returned as a convenience
// to avoid having to decode the payment request in order to retrieve
// its payment hash.
NewChallenge() (string, lntypes.Hash, error)
}
// SecretStore is the store responsible for storing LSAT secrets. These secrets
// are required for proper verification of each minted LSAT.
type SecretStore interface {
// NewSecret creates a new cryptographically random secret which is
// keyed by the given hash.
NewSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error)
// GetSecret returns the cryptographically random secret that
// corresponds to the given hash. If there is no secret, then
// ErrSecretNotFound is returned.
GetSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error)
// RevokeSecret removes the cryptographically random secret that
// corresponds to the given hash. This acts as a NOP if the secret does
// not exist.
RevokeSecret(context.Context, [sha256.Size]byte) error
}
// ServiceLimiter abstracts the source of caveats that should be applied to an
// LSAT for a particular service.
type ServiceLimiter interface {
// ServiceCapabilities returns the capabilities caveats for each
// service. This determines which capabilities of each service can be
// accessed.
ServiceCapabilities(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
// ServiceConstraints returns the constraints for each service. This
// enforces additional constraints on a particular service/service
// capability.
ServiceConstraints(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
}
// Config packages all of the required dependencies to instantiate a new LSAT
// mint.
type Config struct {
// Secrets is our source for LSAT secrets which will be used for
// verification purposes.
Secrets SecretStore
// Challenger is our source of new challenges to present requesters of
// an LSAT with.
Challenger Challenger
// ServiceLimiter provides us with how we should limit a new LSAT based
// on its target services.
ServiceLimiter ServiceLimiter
}
// Mint is an entity that is able to mint and verify LSATs for a set of
// services.
type Mint struct {
cfg Config
}
// New creates a new LSAT mint backed by its given dependencies.
func New(cfg *Config) *Mint {
return &Mint{cfg: *cfg}
}
// MintLSAT mints a new LSAT for the target services.
func (m *Mint) MintLSAT(ctx context.Context,
services ...lsat.Service) (*macaroon.Macaroon, string, error) {
// We'll start by retrieving a new challenge in the form of a Lightning
// payment request to present the requester of the LSAT with.
paymentRequest, paymentHash, err := m.cfg.Challenger.NewChallenge()
if err != nil {
return nil, "", err
}
// TODO(wilmer): remove invoice if any of the operations below fail?
// We can then proceed to mint the LSAT with a unique identifier that is
// mapped to a unique secret.
id, err := createUniqueIdentifier(paymentHash)
if err != nil {
return nil, "", err
}
idHash := sha256.Sum256(id)
secret, err := m.cfg.Secrets.NewSecret(ctx, idHash)
if err != nil {
return nil, "", err
}
macaroon, err := macaroon.New(
secret[:], id, "lsat", macaroon.LatestVersion,
)
if err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
// Include any restrictions that should be immediately applied to the
// LSAT.
var caveats []lsat.Caveat
if len(services) > 0 {
var err error
caveats, err = m.caveatsForServices(ctx, services...)
if err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
}
if err := lsat.AddFirstPartyCaveats(macaroon, caveats...); err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
return macaroon, paymentRequest, nil
}
// createUniqueIdentifier creates a new LSAT identifier bound to a payment hash
// and a randomly generated ID.
func createUniqueIdentifier(paymentHash lntypes.Hash) ([]byte, error) {
tokenID, err := generateTokenID()
if err != nil {
return nil, err
}
id := &lsat.Identifier{
Version: lsat.LatestVersion,
PaymentHash: paymentHash,
TokenID: tokenID,
}
var buf bytes.Buffer
if err := lsat.EncodeIdentifier(&buf, id); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// generateTokenID generates a new random LSAT ID.
func generateTokenID() ([lsat.TokenIDSize]byte, error) {
var tokenID [lsat.TokenIDSize]byte
_, err := rand.Read(tokenID[:])
return tokenID, err
}
// caveatsForServices returns all of the caveats that should be applied to an
// LSAT for the target services.
func (m *Mint) caveatsForServices(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
servicesCaveat, err := lsat.NewServicesCaveat(services...)
if err != nil {
return nil, err
}
capabilities, err := m.cfg.ServiceLimiter.ServiceCapabilities(
ctx, services...,
)
if err != nil {
return nil, err
}
constraints, err := m.cfg.ServiceLimiter.ServiceConstraints(
ctx, services...,
)
if err != nil {
return nil, err
}
caveats := []lsat.Caveat{servicesCaveat}
caveats = append(caveats, capabilities...)
caveats = append(caveats, constraints...)
return caveats, nil
}
// VerificationParams holds all of the requirements to properly verify an LSAT.
type VerificationParams struct {
// Macaroon is the macaroon as part of the LSAT we'll attempt to verify.
Macaroon *macaroon.Macaroon
// Preimage is the preimage that should correspond to the LSAT's payment
// hash.
Preimage lntypes.Preimage
// TargetService is the target service a user of an LSAT is attempting
// to access.
TargetService string
}
// VerifyLSAT attempts to verify an LSAT with the given parameters.
func (m *Mint) VerifyLSAT(ctx context.Context, params *VerificationParams) error {
// We'll first perform a quick check to determine if a valid preimage
// was provided.
id, err := lsat.DecodeIdentifier(bytes.NewReader(params.Macaroon.Id()))
if err != nil {
return err
}
if params.Preimage.Hash() != id.PaymentHash {
return fmt.Errorf("invalid preimage %v for %v", params.Preimage,
id.PaymentHash)
}
// If there was, then we'll ensure the LSAT was minted by us.
secret, err := m.cfg.Secrets.GetSecret(
ctx, sha256.Sum256(params.Macaroon.Id()),
)
if err != nil {
return err
}
rawCaveats, err := params.Macaroon.VerifySignature(secret[:], nil)
if err != nil {
return err
}
// With the LSAT verified, we'll now inspect its caveats to ensure the
// target service is authorized.
var caveats []lsat.Caveat
for _, rawCaveat := range rawCaveats {
// LSATs can contain third-party caveats that we're not aware
// of, so just skip those.
caveat, err := lsat.DecodeCaveat(rawCaveat)
if err != nil {
continue
}
caveats = append(caveats, caveat)
}
return lsat.VerifyCaveats(
caveats, lsat.NewServicesSatisfier(params.TargetService),
)
}

227
mint/mint_test.go Normal file
View File

@@ -0,0 +1,227 @@
package mint
import (
"context"
"crypto/sha256"
"strings"
"testing"
"github.com/lightninglabs/loop/lsat"
"gopkg.in/macaroon.v2"
)
var (
testService = lsat.Service{
Name: "lightning_loop",
Tier: lsat.BaseTier,
}
)
// TestBasicLSAT ensures that an LSAT can only access the services it's
// authorized to.
func TestBasicLSAT(t *testing.T) {
t.Parallel()
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
})
// Mint a basic LSAT which is only able to access the given service.
macaroon, _, err := mint.MintLSAT(ctx, testService)
if err != nil {
t.Fatalf("unable to mint LSAT: %v", err)
}
params := VerificationParams{
Macaroon: macaroon,
Preimage: testPreimage,
TargetService: testService.Name,
}
if err := mint.VerifyLSAT(ctx, &params); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
// It should not be able to access an unknown service.
unknownParams := params
unknownParams.TargetService = "uknown"
err = mint.VerifyLSAT(ctx, &unknownParams)
if !strings.Contains(err.Error(), "not authorized") {
t.Fatal("expected LSAT to not be authorized")
}
}
// TestAdminLSAT ensures that an admin LSAT (one without a services caveat) is
// authorized to access any service.
func TestAdminLSAT(t *testing.T) {
t.Parallel()
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
})
// Mint an admin LSAT by not including any services.
macaroon, _, err := mint.MintLSAT(ctx)
if err != nil {
t.Fatalf("unable to mint LSAT: %v", err)
}
// It should be able to access any service as it doesn't have a services
// caveat.
params := &VerificationParams{
Macaroon: macaroon,
Preimage: testPreimage,
TargetService: testService.Name,
}
if err := mint.VerifyLSAT(ctx, params); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
}
// TestRevokedLSAT ensures that we can no longer verify a revoked LSAT.
func TestRevokedLSAT(t *testing.T) {
t.Parallel()
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
})
// Mint an LSAT and verify it.
lsat, _, err := mint.MintLSAT(ctx)
if err != nil {
t.Fatalf("unable to mint LSAT: %v", err)
}
params := &VerificationParams{
Macaroon: lsat,
Preimage: testPreimage,
TargetService: testService.Name,
}
if err := mint.VerifyLSAT(ctx, params); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
// Proceed to revoke it. We should no longer be able to verify it after.
idHash := sha256.Sum256(lsat.Id())
if err := mint.cfg.Secrets.RevokeSecret(ctx, idHash); err != nil {
t.Fatalf("unable to revoke LSAT: %v", err)
}
if err := mint.VerifyLSAT(ctx, params); err != ErrSecretNotFound {
t.Fatalf("expected ErrSecretNotFound, got %v", err)
}
}
// TestTamperedLSAT ensures that an LSAT that has been tampered with by
// modifying its signature results in its verification failing.
func TestTamperedLSAT(t *testing.T) {
t.Parallel()
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
})
// Mint a new LSAT and verify it is valid.
mac, _, err := mint.MintLSAT(ctx, testService)
if err != nil {
t.Fatalf("unable to mint LSAT: %v", err)
}
params := VerificationParams{
Macaroon: mac,
Preimage: testPreimage,
TargetService: testService.Name,
}
if err := mint.VerifyLSAT(ctx, &params); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
// Create a tampered LSAT from the valid one.
macBytes, err := mac.MarshalBinary()
if err != nil {
t.Fatalf("unable to serialize macaroon: %v", err)
}
macBytes[len(macBytes)-1] = 0x00
var tampered macaroon.Macaroon
if err := tampered.UnmarshalBinary(macBytes); err != nil {
t.Fatalf("unable to deserialize macaroon: %v", err)
}
// Attempting to verify the tampered LSAT should fail.
tamperedParams := params
tamperedParams.Macaroon = &tampered
err = mint.VerifyLSAT(ctx, &tamperedParams)
if !strings.Contains(err.Error(), "signature mismatch") {
t.Fatal("expected tampered LSAT to be invalid")
}
}
// TestDemotedServicesLSAT ensures that an LSAT which originally was authorized
// to access a service, but was then demoted to no longer be the case, is no
// longer authorized.
func TestDemotedServicesLSAT(t *testing.T) {
t.Parallel()
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
})
unauthorizedService := testService
unauthorizedService.Name = "unauthorized"
// Mint an LSAT that is able to access two services, one of which will
// be denied later on.
mac, _, err := mint.MintLSAT(ctx, testService, unauthorizedService)
if err != nil {
t.Fatalf("unable to mint LSAT: %v", err)
}
// It should be able to access both services.
authorizedParams := VerificationParams{
Macaroon: mac,
Preimage: testPreimage,
TargetService: testService.Name,
}
if err := mint.VerifyLSAT(ctx, &authorizedParams); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
unauthorizedParams := VerificationParams{
Macaroon: mac,
Preimage: testPreimage,
TargetService: unauthorizedService.Name,
}
if err := mint.VerifyLSAT(ctx, &unauthorizedParams); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
// Demote the second service by including an additional services caveat
// that only includes the first service.
services, err := lsat.NewServicesCaveat(testService)
if err != nil {
t.Fatalf("unable to create services caveat: %v", err)
}
err = lsat.AddFirstPartyCaveats(mac, services)
if err != nil {
t.Fatalf("unable to demote LSAT: %v", err)
}
// It should now only be able to access the first, but not the second.
if err := mint.VerifyLSAT(ctx, &authorizedParams); err != nil {
t.Fatalf("unable to verify LSAT: %v", err)
}
err = mint.VerifyLSAT(ctx, &unauthorizedParams)
if !strings.Contains(err.Error(), "not authorized") {
t.Fatal("expected macaroon to be invalid")
}
}

113
mint/mock_test.go Normal file
View File

@@ -0,0 +1,113 @@
package mint
import (
"context"
"crypto/sha256"
"math/rand"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
testPreimage = lntypes.Preimage{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
}
testHash = testPreimage.Hash()
testPayReq = "lnsb1..."
)
type mockChallenger struct{}
var _ Challenger = (*mockChallenger)(nil)
func newMockChallenger() *mockChallenger {
return &mockChallenger{}
}
func (d *mockChallenger) NewChallenge() (string, lntypes.Hash, error) {
return testPayReq, testHash, nil
}
type mockSecretStore struct {
secrets map[[sha256.Size]byte][lsat.SecretSize]byte
}
var _ SecretStore = (*mockSecretStore)(nil)
func (s *mockSecretStore) NewSecret(ctx context.Context,
id [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
var secret [lsat.SecretSize]byte
if _, err := rand.Read(secret[:]); err != nil {
return secret, err
}
s.secrets[id] = secret
return secret, nil
}
func (s *mockSecretStore) GetSecret(ctx context.Context,
id [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
secret, ok := s.secrets[id]
if !ok {
return secret, ErrSecretNotFound
}
return secret, nil
}
func (s *mockSecretStore) RevokeSecret(ctx context.Context,
id [sha256.Size]byte) error {
delete(s.secrets, id)
return nil
}
func newMockSecretStore() *mockSecretStore {
return &mockSecretStore{
secrets: make(map[[sha256.Size]byte][lsat.SecretSize]byte),
}
}
type mockServiceLimiter struct {
capabilities map[lsat.Service]lsat.Caveat
constraints map[lsat.Service][]lsat.Caveat
}
var _ ServiceLimiter = (*mockServiceLimiter)(nil)
func newMockServiceLimiter() *mockServiceLimiter {
return &mockServiceLimiter{
capabilities: make(map[lsat.Service]lsat.Caveat),
constraints: make(map[lsat.Service][]lsat.Caveat),
}
}
func (l *mockServiceLimiter) ServiceCapabilities(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
var res []lsat.Caveat
for _, service := range services {
capabilities, ok := l.capabilities[service]
if !ok {
continue
}
res = append(res, capabilities)
}
return res, nil
}
func (l *mockServiceLimiter) ServiceConstraints(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
var res []lsat.Caveat
for _, service := range services {
constraints, ok := l.constraints[service]
if !ok {
continue
}
res = append(res, constraints...)
}
return res, nil
}

View File

@@ -93,15 +93,15 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// accordingly.
switch {
case target.Auth.IsOn():
if !p.authenticator.Accept(&r.Header) {
if !p.authenticator.Accept(&r.Header, target.Name) {
prefixLog.Infof("Authentication failed. Sending 402.")
p.handlePaymentRequired(w, r)
p.handlePaymentRequired(w, r, target.Name)
return
}
case target.Auth.IsFreebie():
// We only need to respect the freebie counter if the user
// is not authenticated at all.
if !p.authenticator.Accept(&r.Header) {
if !p.authenticator.Accept(&r.Header, target.Name) {
ok, err := target.freebieDb.CanPass(r, remoteIp)
if err != nil {
prefixLog.Errorf("Error querying freebie db: "+
@@ -113,7 +113,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if !ok {
p.handlePaymentRequired(w, r)
p.handlePaymentRequired(w, r, target.Name)
return
}
_, err = target.freebieDb.TallyFreebie(r, remoteIp)
@@ -278,10 +278,12 @@ func addCorsHeaders(header http.Header) {
// handlePaymentRequired returns fresh challenge header fields and status code
// to the client signaling that a payment is required to fulfil the request.
func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) {
func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request,
serviceName string) {
addCorsHeaders(r.Header)
header, err := p.authenticator.FreshChallengeHeader(r)
header, err := p.authenticator.FreshChallengeHeader(r, serviceName)
if err != nil {
log.Errorf("Error creating new challenge header: %v", err)
sendDirectResponse(

View File

@@ -20,6 +20,9 @@ var (
// Service generically specifies configuration data for backend services to the
// Kirin proxy.
type Service struct {
// Name is the name of the LSAT-enabled service.
Name string `long:"name" description:"Name of the LSAT-enabled service"`
// TLSCertPath is the optional path to the service's TLS certificate.
TLSCertPath string `long:"tlscertpath" description:"Path to the service's TLS certificate"`
@@ -55,6 +58,15 @@ type Service struct {
// the file is sent encoded as base64.
Headers map[string]string `long:"headers" description:"Header fields to always pass to the service"`
// Capabilities is the list of capabilities authorized for the service
// at the base tier.
Capabilities string `long:"capabilities" description:"A comma-separated list of the service capabilities authorized for the base tier"`
// Constraints is the set of constraints that will take form of caveats.
// They'll be enforced for a service at the base tier. The key should
// correspond to the caveat's condition.
Constraints map[string]string `long:"constraints" description:"The service constraints to enforce at the base tier"`
freebieDb freebie.DB
}

View File

@@ -2,6 +2,17 @@ listenaddr: "localhost:8081"
staticroot: "./static"
debuglevel: "debug"
authenticator:
lndhost: "localhost:10009"
tlspath: "/path/to/lnd/tls.cert"
macdir: "/path/to/lnd/data/chain/bitcoin/simnet"
network: "simnet"
etcd:
host: "localhost:2379"
user: "user"
password: "password"
services:
# List of services that should be reachable behind the proxy.
# Requests will be matched to the services in order, picking the first
@@ -10,13 +21,18 @@ services:
#
# Use single quotes for regular expressions with special characters in them to
# avoid YAML parsing errors!
- hostregexp: '^service1.com$'
- name: "service1"
hostregexp: '^service1.com$'
pathregexp: '^/.*$'
address: "127.0.0.1:10009"
protocol: https
tlscertpath: "path-to-optional-tls-cert/tls.cert"
capabilities: "add,subtract"
- hostregexp: "service2.com:8083"
- name: "service2"
hostregexp: "service2.com:8083"
pathregexp: '^/.*$'
address: "123.456.789:8082"
protocol: https
constraints:
"valid_until": "2020-01-01"

92
secrets.go Normal file
View File

@@ -0,0 +1,92 @@
package kirin
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"github.com/coreos/etcd/clientv3"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lsat"
)
var (
// secretsPrefix is the key we'll use to prefix all LSAT identifiers
// with when storing secrets in an etcd cluster.
secretsPrefix = "secrets"
)
// idKey returns the full key to store in the database for an LSAT identifier.
// The identifier is hex-encoded in order to prevent conflicts with the etcd key
// delimeter.
//
// The resulting path of the identifier bff4ee83 within etcd would look like:
// lsat/proxy/secrets/bff4ee83
func idKey(id [sha256.Size]byte) string {
return strings.Join(
[]string{topLevelKey, secretsPrefix, hex.EncodeToString(id[:])},
etcdKeyDelimeter,
)
}
// secretStore is a store of LSAT secrets backed by an etcd cluster.
type secretStore struct {
*clientv3.Client
}
// A compile-time constraint to ensure secretStore implements mint.SecretStore.
var _ mint.SecretStore = (*secretStore)(nil)
// newSecretStore instantiates a new LSAT secrets store backed by an etcd
// cluster.
func newSecretStore(client *clientv3.Client) *secretStore {
return &secretStore{Client: client}
}
// NewSecret creates a new cryptographically random secret which is keyed by the
// given hash.
func (s *secretStore) NewSecret(ctx context.Context,
id [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
var secret [lsat.SecretSize]byte
if _, err := rand.Read(secret[:]); err != nil {
return secret, err
}
_, err := s.Put(ctx, idKey(id), string(secret[:]))
return secret, err
}
// GetSecret returns the cryptographically random secret that corresponds to the
// given hash. If there is no secret, then mint.ErrSecretNotFound is returned.
func (s *secretStore) GetSecret(ctx context.Context,
id [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
resp, err := s.Get(ctx, idKey(id))
if err != nil {
return [lsat.SecretSize]byte{}, err
}
if len(resp.Kvs) == 0 {
return [lsat.SecretSize]byte{}, mint.ErrSecretNotFound
}
if len(resp.Kvs[0].Value) != lsat.SecretSize {
return [lsat.SecretSize]byte{}, fmt.Errorf("invalid secret "+
"size %v", len(resp.Kvs[0].Value))
}
var secret [lsat.SecretSize]byte
copy(secret[:], resp.Kvs[0].Value)
return secret, nil
}
// RevokeSecret removes the cryptographically random secret that corresponds to
// the given hash. This acts as a NOP if the secret does not exist.
func (s *secretStore) RevokeSecret(ctx context.Context,
id [sha256.Size]byte) error {
_, err := s.Delete(ctx, idKey(id))
return err
}

115
secrets_test.go Normal file
View File

@@ -0,0 +1,115 @@
package kirin
import (
"bytes"
"context"
"crypto/sha256"
"io/ioutil"
"net/url"
"os"
"testing"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/embed"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/loop/lsat"
)
// etcdSetup is a helper that instantiates a new etcd cluster along with a
// client connection to it. A cleanup closure is also returned to free any
// allocated resources required by etcd.
func etcdSetup(t *testing.T) (*clientv3.Client, func()) {
t.Helper()
tempDir, err := ioutil.TempDir("", "etcd")
if err != nil {
t.Fatalf("unable to create temp dir: %v", err)
}
cfg := embed.NewConfig()
cfg.Dir = tempDir
cfg.LCUrls = []url.URL{{Host: "127.0.0.1:9125"}}
cfg.LPUrls = []url.URL{{Host: "127.0.0.1:9126"}}
etcd, err := embed.StartEtcd(cfg)
if err != nil {
os.RemoveAll(tempDir)
t.Fatalf("unable to start etcd: %v", err)
}
select {
case <-etcd.Server.ReadyNotify():
case <-time.After(5 * time.Second):
os.RemoveAll(tempDir)
etcd.Server.Stop() // trigger a shutdown
t.Fatal("server took too long to start")
}
client, err := clientv3.New(clientv3.Config{
Endpoints: []string{cfg.LCUrls[0].Host},
DialTimeout: 5 * time.Second,
})
if err != nil {
t.Fatalf("unable to connect to etcd: %v", err)
}
return client, func() {
etcd.Close()
os.RemoveAll(tempDir)
}
}
// assertSecretExists is a helper to determine if a secret for the given
// identifier exists in the store. If it exists, its value is compared against
// the expected secret.
func assertSecretExists(t *testing.T, store *secretStore, id [sha256.Size]byte,
expSecret *[lsat.SecretSize]byte) {
t.Helper()
exists := expSecret != nil
secret, err := store.GetSecret(context.Background(), id)
switch {
case exists && err != nil:
t.Fatalf("unable to retrieve secret: %v", err)
case !exists && err != mint.ErrSecretNotFound:
t.Fatalf("expected error ErrSecretNotFound, got \"%v\"", err)
case exists:
if secret != *expSecret {
t.Fatalf("expected secret %x, got %x", expSecret, secret)
}
default:
return
}
}
// TestSecretStore ensures the different operations of the secretStore behave as
// expected.
func TestSecretStore(t *testing.T) {
etcdClient, serverCleanup := etcdSetup(t)
defer etcdClient.Close()
defer serverCleanup()
ctx := context.Background()
store := newSecretStore(etcdClient)
// Create a test ID and ensure a secret doesn't exist for it yet as we
// haven't created one.
var id [sha256.Size]byte
copy(id[:], bytes.Repeat([]byte("A"), 32))
assertSecretExists(t, store, id, nil)
// Create one and ensure we can retrieve it at a later point.
secret, err := store.NewSecret(ctx, id)
if err != nil {
t.Fatalf("unable to generate new secret: %v", err)
}
assertSecretExists(t, store, id, &secret)
// Once revoked, it should no longer exist.
if err := store.RevokeSecret(ctx, id); err != nil {
t.Fatalf("unable to revoke secret: %v", err)
}
assertSecretExists(t, store, id, nil)
}

78
services.go Normal file
View File

@@ -0,0 +1,78 @@
package kirin
import (
"context"
"github.com/lightninglabs/kirin/mint"
"github.com/lightninglabs/kirin/proxy"
"github.com/lightninglabs/loop/lsat"
)
// staticServiceLimiter provides static restrictions for services.
//
// TODO(wilmer): use etcd instead.
type staticServiceLimiter struct {
capabilities map[lsat.Service]lsat.Caveat
constraints map[lsat.Service][]lsat.Caveat
}
// A compile-time constraint to ensure staticServiceLimiter implements
// mint.ServiceLimiter.
var _ mint.ServiceLimiter = (*staticServiceLimiter)(nil)
// newStaticServiceLimiter instantiates a new static service limiter backed by
// the given restrictions.
func newStaticServiceLimiter(proxyServices []*proxy.Service) *staticServiceLimiter {
capabilities := make(map[lsat.Service]lsat.Caveat)
constraints := make(map[lsat.Service][]lsat.Caveat)
for _, proxyService := range proxyServices {
s := lsat.Service{Name: proxyService.Name, Tier: lsat.BaseTier}
capabilities[s] = lsat.NewCapabilitiesCaveat(
proxyService.Name, proxyService.Capabilities,
)
for cond, value := range proxyService.Constraints {
caveat := lsat.Caveat{Condition: cond, Value: value}
constraints[s] = append(constraints[s], caveat)
}
}
return &staticServiceLimiter{
capabilities: capabilities,
constraints: constraints,
}
}
// ServiceCapabilities returns the capabilities caveats for each service. This
// determines which capabilities of each service can be accessed.
func (l *staticServiceLimiter) ServiceCapabilities(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
var res []lsat.Caveat
for _, service := range services {
capabilities, ok := l.capabilities[service]
if !ok {
continue
}
res = append(res, capabilities)
}
return res, nil
}
// ServiceConstraints returns the constraints for each service. This enforces
// additional constraints on a particular service/service capability.
func (l *staticServiceLimiter) ServiceConstraints(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
var res []lsat.Caveat
for _, service := range services {
constraints, ok := l.constraints[service]
if !ok {
continue
}
res = append(res, constraints...)
}
return res, nil
}