diff --git a/auth/authenticator.go b/auth/authenticator.go index ec51780..be91a1f 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -10,6 +10,21 @@ import ( "github.com/lightninglabs/kirin/macaroons" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" + "gopkg.in/macaroon.v2" +) + +const ( + // HeaderAuthorization is the HTTP header field name that is used to + // send the LSAT by REST clients. + HeaderAuthorization = "Authorization" + + // HeaderMacaroonMD is the HTTP header field name that is used to send + // the LSAT by certain REST and gRPC clients. + HeaderMacaroonMD = "Grpc-Metadata-Macaroon" + + // HeaderMacaroon is the HTTP header field name that is used to send the + // LSAT by our own gRPC clients. + HeaderMacaroon = "Macaroon" ) var ( @@ -47,33 +62,12 @@ func NewLsatAuthenticator(challenger Challenger) (*LsatAuthenticator, error) { // // NOTE: This is part of the Authenticator interface. func (l *LsatAuthenticator) Accept(header *http.Header) bool { - authHeader := header.Get("Authorization") - log.Debugf("Trying to authorize with header value [%s].", authHeader) - if authHeader == "" { - return false - } - - if !authRegex.MatchString(authHeader) { - log.Debugf("Deny: Auth header in invalid format.") - return false - } - - matches := authRegex.FindStringSubmatch(authHeader) - if len(matches) != 3 { - log.Debugf("Deny: Auth header in invalid format.") - return false - } - - macBase64, preimageHex := matches[1], matches[2] - macBytes, err := base64.StdEncoding.DecodeString(macBase64) + // 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 := authFromHeader(header) if err != nil { - log.Debugf("Deny: Base64 decode of macaroon failed: %v", err) - return false - } - - preimageBytes, err := hex.DecodeString(preimageHex) - if err != nil { - log.Debugf("Deny: Hex decode of preimage failed: %v", err) + log.Debugf("Deny: %v", err) return false } @@ -84,7 +78,7 @@ func (l *LsatAuthenticator) Accept(header *http.Header) bool { return false } - err = l.macService.ValidateMacaroon(macBytes, []bakery.Op{}) + err = l.macService.ValidateMacaroon(mac, []bakery.Op{}) if err != nil { log.Debugf("Deny: Macaroon validation failed: %v", err) return false @@ -130,3 +124,95 @@ func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( log.Debugf("Created new challenge header: [%s]", str) return header, nil } + +// authFromHeader tries to extract authentication information from HTTP headers. +// There are two supported formats that can be sent in three different header +// fields: +// 1. Authorization: LSAT : +// 2. Grpc-Metadata-Macaroon: +// 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 authFromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) { + var authHeader string + + switch { + // Header field 1 contains the macaroon and the preimage as distinct + // values separated by a colon. + case header.Get(HeaderAuthorization) != "": + // Parse the content of the header field and check that it is in + // the correct format. + authHeader = header.Get(HeaderAuthorization) + 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) + } + matches := authRegex.FindStringSubmatch(authHeader) + if len(matches) != 3 { + return nil, nil, 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) + } + mac := &macaroon.Macaroon{} + err = mac.UnmarshalBinary(macBytes) + if err != nil { + return nil, nil, fmt.Errorf("unable to unmarshal " + + "macaroon: %v", err) + } + preimageBytes, err := hex.DecodeString(preimageHex) + if err != nil { + return nil, nil, 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 + + // Header field 2: Contains only the macaroon. + case header.Get(HeaderMacaroonMD) != "": + authHeader = header.Get(HeaderMacaroonMD) + + // Header field 3: Contains only the macaroon. + case header.Get(HeaderMacaroon) != "": + authHeader = header.Get(HeaderMacaroon) + + default: + return nil, nil, 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) + } + mac := &macaroon.Macaroon{} + err = mac.UnmarshalBinary(macBytes) + if err != nil { + return nil, nil, 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) + } + preimageBytes, err := hex.DecodeString(preimageHex) + if err != nil { + return nil, nil, fmt.Errorf("hex decode of preimage "+ + "failed: %v", err) + } + + return mac, preimageBytes, nil +} diff --git a/auth/authenticator_test.go b/auth/authenticator_test.go new file mode 100644 index 0000000..83057bd --- /dev/null +++ b/auth/authenticator_test.go @@ -0,0 +1,150 @@ +package auth_test + +import ( + "encoding/base64" + "encoding/hex" + "net/http" + "testing" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/macaroons" + "github.com/lightningnetwork/lnd/lntypes" + "gopkg.in/macaroon.v2" +) + +type mockChallenger struct{} + +func (c *mockChallenger) NewChallenge() (string, lntypes.Hash, error) { + return "lnt1xxxx", lntypes.ZeroHash, nil +} + +// createDummyMacHex creates a valid macaroon with dummy content for our tests. +func createDummyMacHex(preimage string) string { + dummyMac, err := macaroon.New( + []byte("aabbccddeeff00112233445566778899"), []byte("AA=="), + "kirin", macaroon.LatestVersion, + ) + if err != nil { + panic(err) + } + err = dummyMac.AddFirstPartyCaveat( + []byte(macaroons.CondPreimage + " " + preimage), + ) + if err != nil { + panic(err) + } + macBytes, err := dummyMac.MarshalBinary() + if err != nil { + panic(err) + } + return hex.EncodeToString(macBytes) +} + +// TestLsatAuthenticator tests that the authenticator properly handles auth +// headers and the tokens contained in them. +func TestLsatAuthenticator(t *testing.T) { + var ( + testPreimage = "49349dfea4abed3cd14f6d356afa83de" + + "9787b609f088c8df09bacc7b4bd21b39" + testMacHex = createDummyMacHex(testPreimage) + testMacBytes, _ = hex.DecodeString(testMacHex) + testMacBase64 = base64.StdEncoding.EncodeToString( + testMacBytes, + ) + headerTests = []struct { + id string + header *http.Header + result bool + }{ + { + id: "empty header", + header: &http.Header{}, + result: false, + }, + { + id: "no auth header", + header: &http.Header{ + "Test": []string{"foo"}, + }, + result: false, + }, + { + id: "empty auth header", + header: &http.Header{ + auth.HeaderAuthorization: []string{}, + }, + result: false, + }, + { + id: "zero length auth header", + header: &http.Header{ + auth.HeaderAuthorization: []string{""}, + }, + result: false, + }, + { + id: "invalid auth header", + header: &http.Header{ + auth.HeaderAuthorization: []string{ + "foo", + }, + }, + result: false, + }, + { + id: "invalid macaroon metadata header", + header: &http.Header{ + auth.HeaderMacaroonMD: []string{"foo"}, + }, + result: false, + }, + { + id: "invalid macaroon header", + header: &http.Header{ + auth.HeaderMacaroon: []string{"foo"}, + }, + result: false, + }, + { + id: "valid auth header", + header: &http.Header{ + auth.HeaderAuthorization: []string{ + "LSAT " + testMacBase64 + ":" + + testPreimage, + }, + }, + result: true, + }, + { + id: "valid macaroon metadata header", + header: &http.Header{ + auth.HeaderMacaroonMD: []string{ + testMacHex, + }}, + result: true, + }, + { + id: "valid macaroon header", + header: &http.Header{ + auth.HeaderMacaroon: []string{ + testMacHex, + }, + }, + result: true, + }, + } + ) + + a, err := auth.NewLsatAuthenticator(&mockChallenger{}) + if err != nil { + t.Fatalf("Could not create authenticator: %v", err) + } + + for _, testCase := range headerTests { + result := a.Accept(testCase.header) + if result != testCase.result { + t.Fatalf("test case %s failed. got %v expected %v", + testCase.id, result, testCase.result) + } + } +} diff --git a/macaroons/service.go b/macaroons/service.go index b253b4f..a102820 100644 --- a/macaroons/service.go +++ b/macaroons/service.go @@ -3,6 +3,7 @@ package macaroons import ( "context" "encoding/hex" + "fmt" "github.com/lightningnetwork/lnd/macaroons" "gopkg.in/macaroon-bakery.v2/bakery" @@ -11,7 +12,11 @@ import ( ) 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 ( @@ -21,6 +26,10 @@ var ( 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) } @@ -35,10 +44,37 @@ func (r *rootKeyStore) RootKey(_ context.Context) (rootKey, id []byte, 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) { @@ -64,42 +100,36 @@ func (s *Service) NewMacaroon(operations []bakery.Op, caveats []string) ( return macBytes, nil } -func (s *Service) ValidateMacaroon(macBytes []byte, - requiredPermissions []bakery.Op) error { - - mac := &macaroon.Macaroon{} - err := mac.UnmarshalBinary(macBytes) - if err != nil { - return err - } +// 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(), requiredPermissions...) + _, err := authChecker.Allow(context.Background(), perms...) return err } -func NewService(checks ...macaroons.Checker) (*Service, error) { - macaroonParams := bakery.BakeryParams{ - Location: "kirin", - RootKeyStore: &rootKeyStore{}, - Locator: nil, - Key: nil, +// 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") } - - 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) + 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 &Service{*svc}, nil + return "", nil } // isRegistered checks to see if the required checker has already been