From c317071270469242c2bf687926d71b2472bc831e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:32 -0800 Subject: [PATCH 01/11] build: downgrade to loop-compatible lnd version This addresses a build issue when importing kirin into nautilus. Co-authored-by: Oliver Gugger --- go.mod | 2 +- go.sum | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1db81df..5e3578d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d 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/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191115230031-4d7a151b4763 github.com/lightningnetwork/lnd/cert v1.0.0 golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea // indirect diff --git a/go.sum b/go.sum index 539edfd..2d76141 100644 --- a/go.sum +++ b/go.sum @@ -146,9 +146,8 @@ github.com/lightninglabs/neutrino v0.11.0 h1:lPpYFCtsfJX2W5zI4pWycPmbbBdr7zU+Baf 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= From abcd7a98280e0df7469cb6e49ea0eab3beae91c1 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:33 -0800 Subject: [PATCH 02/11] build: update to loop version containing loop utilities --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5e3578d..281cc9b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/golang/protobuf v1.3.2 - github.com/lightninglabs/loop v0.2.4-alpha.0.20191116024025-539d6ed9e3e8 + 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 golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect diff --git a/go.sum b/go.sum index 2d76141..726e7d5 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ 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= From 9f291ddbf98a0f1ed8d5b79a1f22f2e0a3dd810d Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:36 -0800 Subject: [PATCH 03/11] mint: introduce proper LSAT creation and verification This package adheres to the agreed upon internal design document of the macaroon portion of an LSAT. It is able to mint LSATs for a set of services at any tier, each containing their desired set of constraints. LSAT verification so far only ensures the that token was minted by us and that the target service attempted to be accessed is authorized according to the white-listed services contained in the token. --- mint/mint.go | 256 ++++++++++++++++++++++++++++++++++++++++++++++ mint/mint_test.go | 227 ++++++++++++++++++++++++++++++++++++++++ mint/mock_test.go | 113 ++++++++++++++++++++ 3 files changed, 596 insertions(+) create mode 100644 mint/mint.go create mode 100644 mint/mint_test.go create mode 100644 mint/mock_test.go diff --git a/mint/mint.go b/mint/mint.go new file mode 100644 index 0000000..6089c06 --- /dev/null +++ b/mint/mint.go @@ -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), + ) +} diff --git a/mint/mint_test.go b/mint/mint_test.go new file mode 100644 index 0000000..dd0b9af --- /dev/null +++ b/mint/mint_test.go @@ -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, ¶ms); 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, ¶ms); 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") + } +} diff --git a/mint/mock_test.go b/mint/mock_test.go new file mode 100644 index 0000000..7dddc18 --- /dev/null +++ b/mint/mock_test.go @@ -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 +} From 401c0e2d3817038b73db7fcaa9a8bcebf49a0e6e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:41 -0800 Subject: [PATCH 04/11] kirin: add etcd client configuration Allows the ability for the proxy to connect to an etcd cluster for any reliable data storage purposes. No data is being stored yet as of this commit, but we'll be storing LSAT secrets at a later commit. One key component in this commit is that we introduce a new top level key that will serve to hold all LSAT proxy-related data. Any nested keys should be prefixed with said top level key. Co-authored-by: Oliver Gugger --- config.go | 8 ++++++ go.mod | 17 ++++++++++++ go.sum | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ kirin.go | 30 ++++++++++++++++++-- sample-conf.yaml | 5 ++++ 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 368b57f..83031c9 100644 --- a/config.go +++ b/config.go @@ -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 diff --git a/go.mod b/go.mod index 281cc9b..50a553a 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,26 @@ 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/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 @@ -16,4 +32,5 @@ require ( gopkg.in/macaroon-bakery.v2 v2.1.0 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.2.2 + sigs.k8s.io/yaml v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 726e7d5..56185ee 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -161,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= @@ -170,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= @@ -177,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= @@ -188,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= @@ -210,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= @@ -221,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= @@ -254,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= @@ -275,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= @@ -295,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= diff --git a/kirin.go b/kirin.go index 5b7459c..2f1206e 100644 --- a/kirin.go +++ b/kirin.go @@ -6,7 +6,9 @@ import ( "net/http" "os" "path/filepath" + "time" + "github.com/coreos/etcd/clientv3" "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" "github.com/lightningnetwork/lnd/build" @@ -15,6 +17,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 +51,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{ @@ -70,7 +93,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) @@ -144,7 +167,10 @@ func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator) ( } // 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) diff --git a/sample-conf.yaml b/sample-conf.yaml index bcf9cd0..cfce589 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -2,6 +2,11 @@ listenaddr: "localhost:8081" staticroot: "./static" debuglevel: "debug" +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 From 02f2a287b0baadb7361065c08e772347769e64b0 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:46 -0800 Subject: [PATCH 05/11] kirin: add etcd-backed secret store This will store the secret of each LSAT minted by the proxy, which is crucial for LSAT verification. The secrets are stored under a new "secrets" key prefixed by the top level LSAT etcd key, and each secret can be found by its unique identifier prefixed with the secrets key. --- secrets.go | 92 ++++++++++++++++++++++++++++++++++++++ secrets_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 secrets.go create mode 100644 secrets_test.go diff --git a/secrets.go b/secrets.go new file mode 100644 index 0000000..194d84d --- /dev/null +++ b/secrets.go @@ -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 +} diff --git a/secrets_test.go b/secrets_test.go new file mode 100644 index 0000000..af93bb6 --- /dev/null +++ b/secrets_test.go @@ -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) +} From 25851ae16df4f33832f7ebb5b12204d74c181285 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:51 -0800 Subject: [PATCH 06/11] kirin: add static service limiter The service limiter holds all of the constraints that should be applied to a given service at the base tier. These are currently static and are parsed from the proxy's configuration file as a temporary work-around. Eventually, we plan to integrate this with etcd as well in order to achieve dynamic service discovery. --- proxy/service.go | 12 ++++++++ sample-conf.yaml | 9 ++++-- services.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 services.go diff --git a/proxy/service.go b/proxy/service.go index e74218a..ec38b26 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -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 } diff --git a/sample-conf.yaml b/sample-conf.yaml index cfce589..bf40c6e 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -15,13 +15,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" diff --git a/services.go b/services.go new file mode 100644 index 0000000..8e068c1 --- /dev/null +++ b/services.go @@ -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 +} From 7b676b8b91dc5c7ce1451d1eeaf350eca88ecb39 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 21 Nov 2019 17:01:56 -0800 Subject: [PATCH 07/11] auth: use concrete preimage type in FromHeader and SetHeader --- auth/authenticator.go | 60 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/auth/authenticator.go b/auth/authenticator.go index e7d2123..47a6f41 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -8,6 +8,7 @@ import ( "regexp" "github.com/lightninglabs/kirin/macaroons" + "github.com/lightningnetwork/lnd/lntypes" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" "gopkg.in/macaroon.v2" @@ -66,7 +67,7 @@ func (l *LsatAuthenticator) Accept(header *http.Header) 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, _, err := FromHeader(header) if err != nil { log.Debugf("Deny: %v", err) return false @@ -74,10 +75,6 @@ func (l *LsatAuthenticator) Accept(header *http.Header) bool { // TODO(guggero): check preimage against payment hash caveat in the // macaroon. - if len(preimageBytes) != 32 { - log.Debugf("Deny: Decoded preimage has invalid length.") - return false - } err = l.macService.ValidateMacaroon(mac, []bakery.Op{}) if err != nil { @@ -134,7 +131,7 @@ func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( // 3. Macaroon: // 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 +144,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 +185,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) + return nil, lntypes.Preimage{}, fmt.Errorf("unable to extract "+ + "preimage from 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) } - 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 +227,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 From 95c405b0c7468da048f9d7cdf0476f35dd6a6511 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 21 Nov 2019 17:02:01 -0800 Subject: [PATCH 08/11] auth+proxy: extend Authenticator methods with target service name The target service name remains unused in its current form, but will be required in order to verify that an incoming request with an LSAT attached is authorized to access the service being attempted. We can derive this from the request's host field, but we choose to extend the methods with the additional parameter in order to prevent parsing the host field again to determine which service is being accessed. --- auth/authenticator.go | 6 +++--- auth/authenticator_test.go | 2 +- auth/interface.go | 4 ++-- auth/mock_authenticator.go | 10 ++++++++-- proxy/proxy.go | 14 ++++++++------ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/auth/authenticator.go b/auth/authenticator.go index 47a6f41..ca14451 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -63,7 +63,7 @@ func NewLsatAuthenticator(challenger Challenger) (*LsatAuthenticator, error) { // 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. @@ -88,8 +88,8 @@ 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 { diff --git a/auth/authenticator_test.go b/auth/authenticator_test.go index 83057bd..6a0442d 100644 --- a/auth/authenticator_test.go +++ b/auth/authenticator_test.go @@ -141,7 +141,7 @@ func TestLsatAuthenticator(t *testing.T) { } 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) diff --git a/auth/interface.go b/auth/interface.go index 00fa8ad..11114da 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -11,11 +11,11 @@ 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. diff --git a/auth/mock_authenticator.go b/auth/mock_authenticator.go index e854bb1..487cb9f 100644 --- a/auth/mock_authenticator.go +++ b/auth/mock_authenticator.go @@ -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 diff --git a/proxy/proxy.go b/proxy/proxy.go index c1c30be..2a7f057 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -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( From 38cd0e7847696ec3c71301f9ad75f562999fff09 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 21 Nov 2019 17:02:07 -0800 Subject: [PATCH 09/11] auth: integrate proper macaroon creation and verification We move the proxy from using its placeholder macaroon creation and verification to instead use the agreed upon macaroon design. Much of this is solely a refactor, but some new functionality has also been introduced as part of integrating the LSAT mint: 1. A request's target service is now verified to ensure its attached LSAT is authorized. 2. The preimage is now checked against the token's committed payment hash to ensure it has been paid for. --- auth/authenticator.go | 65 +++++++++++++++----------------------- auth/authenticator_test.go | 6 +--- auth/interface.go | 14 ++++++++ auth/mock_test.go | 25 +++++++++++++++ kirin.go | 24 ++++++++------ sample-conf.yaml | 6 ++++ 6 files changed, 85 insertions(+), 55 deletions(-) create mode 100644 auth/mock_test.go diff --git a/auth/authenticator.go b/auth/authenticator.go index ca14451..d5ff285 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -1,6 +1,7 @@ package auth import ( + "context" "encoding/base64" "encoding/hex" "fmt" @@ -8,9 +9,9 @@ import ( "regexp" "github.com/lightninglabs/kirin/macaroons" + "github.com/lightninglabs/kirin/mint" + "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/lntypes" - "gopkg.in/macaroon-bakery.v2/bakery" - "gopkg.in/macaroon-bakery.v2/bakery/checkers" "gopkg.in/macaroon.v2" ) @@ -37,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 @@ -47,16 +47,8 @@ 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 @@ -67,20 +59,23 @@ 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, _, 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. - - err = l.macService.ValidateMacaroon(mac, []bakery.Op{}) + verificationParams := &mint.VerificationParams{ + Macaroon: mac, + Preimage: preimage, + TargetService: serviceName, + } + err = l.minter.VerifyLSAT(context.Background(), verificationParams) if err != nil { - log.Debugf("Deny: Macaroon validation failed: %v", err) + log.Debugf("Deny: LSAT validation failed: %v", err) return false } + return true } @@ -91,31 +86,21 @@ func (l *LsatAuthenticator) Accept(header *http.Header, serviceName string) bool 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) diff --git a/auth/authenticator_test.go b/auth/authenticator_test.go index 6a0442d..afd22b5 100644 --- a/auth/authenticator_test.go +++ b/auth/authenticator_test.go @@ -135,11 +135,7 @@ 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, "test") if result != testCase.result { diff --git a/auth/interface.go b/auth/interface.go index 11114da..679fe2a 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -1,9 +1,13 @@ package auth import ( + "context" "net/http" + "github.com/lightninglabs/kirin/mint" + "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/lntypes" + "gopkg.in/macaroon.v2" ) // Authenticator is the generic interface for validating client headers and @@ -24,3 +28,13 @@ type Challenger interface { // 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 +} diff --git a/auth/mock_test.go b/auth/mock_test.go new file mode 100644 index 0000000..cde61c9 --- /dev/null +++ b/auth/mock_test.go @@ -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 +} diff --git a/kirin.go b/kirin.go index 2f1206e..25cd175 100644 --- a/kirin.go +++ b/kirin.go @@ -10,6 +10,7 @@ import ( "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" @@ -69,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), @@ -150,19 +154,19 @@ 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) } diff --git a/sample-conf.yaml b/sample-conf.yaml index bf40c6e..adfa26b 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -2,6 +2,12 @@ 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" From 530894a5ede5b085a51eda4ca544f171527c0201 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 21 Nov 2019 17:02:12 -0800 Subject: [PATCH 10/11] auth: remove unused Challenger interface --- auth/interface.go | 8 -------- challenger.go | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/auth/interface.go b/auth/interface.go index 679fe2a..600a114 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -6,7 +6,6 @@ import ( "github.com/lightninglabs/kirin/mint" "github.com/lightninglabs/loop/lsat" - "github.com/lightningnetwork/lnd/lntypes" "gopkg.in/macaroon.v2" ) @@ -22,13 +21,6 @@ type Authenticator interface { 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 { diff --git a/challenger.go b/challenger.go index 4c79a62..97cfc50 100644 --- a/challenger.go +++ b/challenger.go @@ -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") From 831a41b33e3498722d7d8a4e5d1d9ba4eefd174a Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 21 Nov 2019 17:02:16 -0800 Subject: [PATCH 11/11] macaroons: remove temporary macaroon creation and verification logic --- auth/authenticator.go | 10 +-- auth/authenticator_test.go | 7 +- go.mod | 2 +- macaroons/service.go | 151 ------------------------------------- 4 files changed, 9 insertions(+), 161 deletions(-) delete mode 100644 macaroons/service.go diff --git a/auth/authenticator.go b/auth/authenticator.go index d5ff285..3ba72f9 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -4,11 +4,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "errors" "fmt" "net/http" "regexp" - "github.com/lightninglabs/kirin/macaroons" "github.com/lightninglabs/kirin/mint" "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/lntypes" @@ -187,10 +187,10 @@ func FromHeader(header *http.Header) (*macaroon.Macaroon, lntypes.Preimage, erro return nil, lntypes.Preimage{}, fmt.Errorf("unable to "+ "unmarshal macaroon: %v", err) } - preimageHex, err := macaroons.ExtractCaveat(mac, macaroons.CondPreimage) - if err != nil { - return nil, lntypes.Preimage{}, 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") } preimage, err := lntypes.MakePreimageFromStr(preimageHex) if err != nil { diff --git a/auth/authenticator_test.go b/auth/authenticator_test.go index afd22b5..4467892 100644 --- a/auth/authenticator_test.go +++ b/auth/authenticator_test.go @@ -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) } diff --git a/go.mod b/go.mod index 50a553a..5a8b387 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( 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 diff --git a/macaroons/service.go b/macaroons/service.go deleted file mode 100644 index a102820..0000000 --- a/macaroons/service.go +++ /dev/null @@ -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 -}