mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-02-23 02:14:26 +01:00
auth+macaroons: allow authorization by macaroon only
This commit is contained in:
@@ -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
150
auth/authenticator_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user