auth+macaroons: allow authorization by macaroon only

This commit is contained in:
Oliver Gugger
2019-11-01 16:03:51 +01:00
parent 4b0a83b46a
commit 8f4dfc5d57
3 changed files with 318 additions and 52 deletions

View File

@@ -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 <macBase64>:<preimageHex>
// 2. Grpc-Metadata-Macaroon: <macHex>
// 3. Macaroon: <macHex>
// If only the macaroon is sent in header 2 or three then it is expected to have
// a caveat with the preimage attached to it.
func 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
}

150
auth/authenticator_test.go Normal file
View File

@@ -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)
}
}
}

View File

@@ -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