From 00f661318f217adc9767bc8abc6501dbdab7d7dd Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 31 Oct 2019 13:37:42 +0100 Subject: [PATCH 1/4] proxy: send error as gRPC error --- auth/mock_authenticator.go | 6 +++++ go.mod | 5 ++++ go.sum | 13 ++++++++++ proxy/proxy.go | 50 +++++++++++++++++++++++++++++++------- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/auth/mock_authenticator.go b/auth/mock_authenticator.go index d0b25b6..e854bb1 100644 --- a/auth/mock_authenticator.go +++ b/auth/mock_authenticator.go @@ -16,6 +16,12 @@ func (a MockAuthenticator) Accept(header *http.Header) bool { if header.Get("Authorization") != "" { return true } + if header.Get("Grpc-Metadata-macaroon") != "" { + return true + } + if header.Get("Macaroon") != "" { + return true + } return false } diff --git a/go.mod b/go.mod index 22a9550..d758441 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,13 @@ go 1.13 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.3-alpha github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191029004703-c069bdd4c7c1 + golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect + golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea // indirect + google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a // indirect + google.golang.org/grpc v1.25.1 gopkg.in/macaroon-bakery.v2 v2.1.0 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index 1de0168..baed7e4 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= @@ -65,6 +66,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2 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/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= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= @@ -78,6 +81,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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= @@ -175,6 +179,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/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= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -220,6 +225,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -235,6 +242,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea h1:Mz1TMnfJDRJLk8S8OPCoJYgrsp/Se/2TBre2+vwX128= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -253,11 +262,15 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/proxy/proxy.go b/proxy/proxy.go index 793413b..f2749d0 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -8,8 +8,11 @@ import ( "net/http" "net/http/httputil" "regexp" + "strconv" + "strings" "github.com/lightninglabs/kirin/auth" + "google.golang.org/grpc/codes" ) const ( @@ -18,7 +21,9 @@ const ( // An example entry would look like this: // 2019-11-09 04:07:55.072 [INF] PRXY: 66.249.69.89 - - // "GET /availability/v1/btc.json HTTP/1.1" "" "Mozilla/5.0 ..." - formatPattern = "- - \"%s %s %s\" \"%s\" \"%s\"" + formatPattern = "- - \"%s %s %s\" \"%s\" \"%s\"" + hdrContentType = "Content-Type" + hdrTypeGrpc = "application/grpc" ) // Proxy is a HTTP, HTTP/2 and gRPC handler that takes an incoming request, @@ -68,7 +73,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // any content; if r.Method == "OPTIONS" { addCorsHeaders(w.Header()) - w.WriteHeader(http.StatusOK) + sendDirectResponse(w, r, http.StatusOK, "") return } @@ -101,7 +106,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err != nil { prefixLog.Errorf("Error querying freebie db: "+ "%v", err) - w.WriteHeader(http.StatusInternalServerError) + sendDirectResponse( + w, r, http.StatusInternalServerError, + "freebie DB failure", + ) return } if !ok { @@ -112,7 +120,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err != nil { prefixLog.Errorf("Error updating freebie db: "+ "%v", err) - w.WriteHeader(http.StatusInternalServerError) + sendDirectResponse( + w, r, http.StatusInternalServerError, + "freebie DB failure", + ) return } } @@ -263,8 +274,11 @@ func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { header, err := p.authenticator.FreshChallengeHeader(r) if err != nil { - log.Errorf("Error creating new challenge header, response 500.") - w.WriteHeader(http.StatusInternalServerError) + log.Errorf("Error creating new challenge header: %v", err) + sendDirectResponse( + w, r, http.StatusInternalServerError, + "challenge failure", + ) return } @@ -275,8 +289,26 @@ func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { } } - w.WriteHeader(http.StatusPaymentRequired) - if _, err := w.Write([]byte("payment required")); err != nil { - log.Errorf("Error writing response: %v", err) + sendDirectResponse(w, r, http.StatusPaymentRequired, "payment required") +} + +// sendDirectResponse sends a response directly to the client without proxying +// anything to a backend. The given error is transported in a way the client can +// understand. This means, for a gRPC client it is sent as specific header +// fields. +func sendDirectResponse(w http.ResponseWriter, r *http.Request, + statusCode int, errInfo string) { + + // Find out if the client is a normal HTTP or a gRPC client. Every gRPC + // request should have the Content-Type header field set accordingly + // so we can use that. + switch { + case strings.HasPrefix(r.Header.Get(hdrContentType), hdrTypeGrpc): + w.Header().Set("Grpc-Status", strconv.Itoa(int(codes.Internal))) + w.Header().Set("Grpc-Message", errInfo) + w.WriteHeader(statusCode) + + default: + http.Error(w, errInfo, statusCode) } } From 4b0a83b46abd45a03ba976fb39502d459fe83bd9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 31 Oct 2019 13:38:22 +0100 Subject: [PATCH 2/4] proxy: test HTTP and gRPC proxying --- proxy/proxy_test.go | 370 ++++++++++++++++++++++++++++++++--- proxy/testdata/gen_protos.sh | 8 + proxy/testdata/hello.pb.go | 194 ++++++++++++++++++ proxy/testdata/hello.proto | 15 ++ 4 files changed, 562 insertions(+), 25 deletions(-) create mode 100755 proxy/testdata/gen_protos.sh create mode 100644 proxy/testdata/hello.pb.go create mode 100644 proxy/testdata/hello.proto diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 411f4d0..d049f1c 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -1,58 +1,98 @@ package proxy_test import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io/ioutil" + "math/big" + "net" "net/http" + "os" + "path" "strings" "testing" "time" "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" + proxytest "github.com/lightninglabs/kirin/proxy/testdata" + "github.com/lightningnetwork/lnd/macaroons" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + "gopkg.in/macaroon.v2" ) const ( - testAddr = "localhost:10019" + testProxyAddr = "localhost:10019" testHostRegexp = "^localhost:.*$" - testPathRegexp = "^/grpc/.*$" + testPathRegexpHTTP = "^/http/.*$" + testPathRegexpGRPC = "^/proxy_test.*$" testTargetServiceAddress = "localhost:8082" testHTTPResponseBody = "HTTP Hello" ) -func TestProxy(t *testing.T) { +var ( + serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) + tlsCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } +) + +// helloServer is a simple server that implements the GreeterServer interface. +type helloServer struct{} + +// SayHello returns a simple string that also contains a string from the +// request. +func (s *helloServer) SayHello(ctx context.Context, + req *proxytest.HelloRequest) (*proxytest.HelloReply, error) { + + return &proxytest.HelloReply{ + Message: fmt.Sprintf("Hello %s", req.Name), + }, nil +} + +// TestProxyHTTP tests that the proxy can forward HTTP requests to a backend +// service and handle LSAT authentication correctly. +func TestProxyHTTP(t *testing.T) { // Create a list of services to proxy between. services := []*proxy.Service{{ Address: testTargetServiceAddress, HostRegexp: testHostRegexp, - PathRegexp: testPathRegexp, + PathRegexp: testPathRegexpHTTP, Protocol: "http", }} - auth := auth.NewMockAuthenticator() - proxy, err := proxy.New(auth, services, "static") + mockAuth := auth.NewMockAuthenticator() + p, err := proxy.New(mockAuth, services, "static") if err != nil { t.Fatalf("failed to create new proxy: %v", err) } // Start server that gives requests to the proxy. server := &http.Server{ - Addr: testAddr, - Handler: http.HandlerFunc(proxy.ServeHTTP), + Addr: testProxyAddr, + Handler: http.HandlerFunc(p.ServeHTTP), } - - go func() { - if err := server.ListenAndServe(); err != nil { - t.Fatalf("failed to serve to proxy: %v", err) - } - }() + go server.ListenAndServe() + defer server.Close() // Start the target backend service. - go func() { - if err := startHTTPHello(); err != nil { - t.Fatalf("failed to start backend service: %v", err) - } - }() + backendService := &http.Server{Addr: testTargetServiceAddress} + go startBackendHTTP(backendService) + defer backendService.Close() // Wait for servers to start. time.Sleep(100 * time.Millisecond) @@ -60,7 +100,7 @@ func TestProxy(t *testing.T) { // Test making a request to the backend service without the // Authorization header set. client := &http.Client{} - url := fmt.Sprintf("http://%s/grpc/test", testAddr) + url := fmt.Sprintf("http://%s/http/test", testProxyAddr) resp, err := client.Get(url) if err != nil { t.Fatalf("errored making http request: %v", err) @@ -79,6 +119,9 @@ func TestProxy(t *testing.T) { // Make sure that if the Auth header is set, the client's request is // proxied to the backend service. req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatalf("error creating request: %v", err) + } req.Header.Add("Authorization", "foobar") resp, err = client.Do(req) @@ -103,10 +146,287 @@ func TestProxy(t *testing.T) { } } -func startHTTPHello() error { - sayHello := func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(testHTTPResponseBody)) +// TestProxyHTTP tests that the proxy can forward gRPC requests to a backend +// service and handle LSAT authentication correctly. +func TestProxyGRPC(t *testing.T) { + // Since gRPC only really works over TLS, we need to generate a + // certificate and key pair first. + tempDirName, err := ioutil.TempDir("", "proxytest") + if err != nil { + t.Fatalf("unable to create temp dir: %v", err) + } + certFile := path.Join(tempDirName, "proxy.cert") + keyFile := path.Join(tempDirName, "proxy.key") + certPool, creds, cert, err := genCertPair(certFile, keyFile) + if err != nil { + t.Fatalf("unable to create cert pair: %v", err) + } + + // Create a list of services to proxy between. + services := []*proxy.Service{{ + Address: testTargetServiceAddress, + HostRegexp: testHostRegexp, + PathRegexp: testPathRegexpGRPC, + Protocol: "https", + TLSCertPath: certFile, + }} + + // Create the proxy server and start serving on TLS. + mockAuth := auth.NewMockAuthenticator() + p, err := proxy.New(mockAuth, services, "static") + if err != nil { + t.Fatalf("failed to create new proxy: %v", err) + } + server := &http.Server{ + Addr: testProxyAddr, + Handler: http.HandlerFunc(p.ServeHTTP), + TLSConfig: &tls.Config{ + RootCAs: certPool, + InsecureSkipVerify: true, + }, + } + go server.ListenAndServeTLS(certFile, keyFile) + defer server.Close() + + // Start the target backend service also on TLS. + tlsConf := &tls.Config{ + Certificates: []tls.Certificate{cert}, + CipherSuites: tlsCipherSuites, + MinVersion: tls.VersionTLS12, + } + serverOpts := []grpc.ServerOption{ + grpc.Creds(credentials.NewTLS(tlsConf)), + } + backendService := grpc.NewServer(serverOpts...) + go startBackendGRPC(backendService) + defer backendService.Stop() + + // Dial to the proxy now, without any authentication. + opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} + conn, err := grpc.Dial(testProxyAddr, opts...) + if err != nil { + t.Fatalf("unable to connect to RPC server: %v", err) + } + client := proxytest.NewGreeterClient(conn) + + // Make request without authentication. We expect an error that can + // be parsed by gRPC. + req := &proxytest.HelloRequest{Name: "foo"} + res, err := client.SayHello( + context.Background(), req, grpc.WaitForReady(true), + ) + if err == nil { + t.Fatalf("expected error to be returned without auth") + } + statusErr, ok := status.FromError(err) + if !ok { + t.Fatalf("expected error to be status.Status") + } + if statusErr.Code() != codes.Internal { + t.Fatalf("unexpected code. wanted %d, got %d", + codes.Internal, statusErr.Code()) + } + if statusErr.Message() != "payment required" { + t.Fatalf("invalid error. expected [%s] got [%s]", + "payment required", err.Error()) + } + + // Dial to the proxy again, this time with a dummy macaroon. + dummyMac, err := macaroon.New( + []byte("key"), []byte("id"), "loc", macaroon.LatestVersion, + ) + opts = []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + grpc.WithPerRPCCredentials(macaroons.NewMacaroonCredential( + dummyMac, + )), + } + conn, err = grpc.Dial(testProxyAddr, opts...) + if err != nil { + t.Fatalf("unable to connect to RPC server: %v", err) + } + client = proxytest.NewGreeterClient(conn) + + // Make the request. This time no error should be returned. + req = &proxytest.HelloRequest{Name: "foo"} + res, err = client.SayHello(context.Background(), req) + if err != nil { + t.Fatalf("unable to call service: %v", err) + } + if res.Message != "Hello foo" { + t.Fatalf("unexpected reply, wanted %s, got %s", + "Hello foo", res.Message) } - http.HandleFunc("/", sayHello) - return http.ListenAndServe(testTargetServiceAddress, nil) +} + +// startBackendHTTP starts the given HTTP server and blocks until the server +// is shut down. +func startBackendHTTP(server *http.Server) error { + sayHello := func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(testHTTPResponseBody)) + if err != nil { + panic(err) + } + } + server.Handler = http.HandlerFunc(sayHello) + return server.ListenAndServe() +} + +// startBackendGRPC starts the given RPC server and blocks until the server is +// shut down. +func startBackendGRPC(grpcServer *grpc.Server) error { + server := helloServer{} + proxytest.RegisterGreeterServer(grpcServer, &server) + grpcListener, err := net.Listen("tcp", testTargetServiceAddress) + if err != nil { + return fmt.Errorf("RPC server unable to listen on %s", + testTargetServiceAddress) + + } + return grpcServer.Serve(grpcListener) +} + +// genCertPair generates a pair of private key and certificate and returns them +// in different formats needed to spin up test servers and clients. +func genCertPair(certFile, keyFile string) (*x509.CertPool, + credentials.TransportCredentials, tls.Certificate, error) { + + org := "kirin autogenerated cert" + cert := tls.Certificate{} + now := time.Now() + validUntil := now.Add(1 * time.Hour) + + // Generate a serial number that's below the serialNumberLimit. + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, cert, fmt.Errorf("failed to generate serial "+ + "number: %s", err) + } + + // Collect the host's IP addresses, including loopback, in a slice. + ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + + // addIP appends an IP address only if it isn't already in the slice. + addIP := func(ipAddr net.IP) { + for _, ip := range ipAddresses { + if ip.Equal(ipAddr) { + return + } + } + ipAddresses = append(ipAddresses, ipAddr) + } + + // Add all the interface IPs that aren't already in the slice. + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, nil, cert, err + } + for _, a := range addrs { + ipAddr, _, err := net.ParseCIDR(a.String()) + if err == nil { + addIP(ipAddr) + } + } + + // Collect the host's names into a slice. + host, err := os.Hostname() + if err != nil { + return nil, nil, cert, err + } + dnsNames := []string{host} + if host != "localhost" { + dnsNames = append(dnsNames, "localhost") + } + + // Generate a private key for the certificate. + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, cert, err + } + + // Construct the certificate template. + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{org}, + CommonName: host, + }, + NotBefore: now.Add(-time.Hour * 24), + NotAfter: validUntil, + + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + + DNSNames: dnsNames, + IPAddresses: ipAddresses, + } + + derBytes, err := x509.CreateCertificate( + rand.Reader, &template, &template, &priv.PublicKey, priv, + ) + if err != nil { + return nil, nil, cert, fmt.Errorf("failed to create "+ + "certificate: %v", err) + } + + certBuf := &bytes.Buffer{} + err = pem.Encode( + certBuf, + &pem.Block{Type: "CERTIFICATE", + Bytes: derBytes, + }, + ) + if err != nil { + return nil, nil, cert, fmt.Errorf("failed to encode "+ + "certificate: %v", err) + } + + keybytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, cert, fmt.Errorf("unable to encode privkey: "+ + "%v", err) + } + keyBuf := &bytes.Buffer{} + err = pem.Encode( + keyBuf, + &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keybytes, + }, + ) + if err != nil { + return nil, nil, cert, fmt.Errorf("failed to encode private "+ + "key: %v", err) + } + cert, err = tls.X509KeyPair(certBuf.Bytes(), keyBuf.Bytes()) + if err != nil { + return nil, nil, cert, fmt.Errorf("failed to create key pair: "+ + "%v", err) + } + + // Write cert and key files. + if err = ioutil.WriteFile(certFile, certBuf.Bytes(), 0644); err != nil { + return nil, nil, cert, fmt.Errorf("unable to write cert file "+ + "at %v: %v", certFile, err) + } + if err = ioutil.WriteFile(keyFile, keyBuf.Bytes(), 0600); err != nil { + os.Remove(certFile) + return nil, nil, cert, fmt.Errorf("unable to write key file "+ + "at %v: %v", keyFile, err) + } + + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(certBuf.Bytes()) { + return nil, nil, cert, fmt.Errorf("credentials: failed to " + + "append certificate") + } + + creds, err := credentials.NewClientTLSFromFile(certFile, "") + if err != nil { + return nil, nil, cert, fmt.Errorf("unable to load cert file: "+ + "%v", err) + } + return cp, creds, cert, nil } diff --git a/proxy/testdata/gen_protos.sh b/proxy/testdata/gen_protos.sh new file mode 100755 index 0000000..1e9032f --- /dev/null +++ b/proxy/testdata/gen_protos.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +protoc -I/usr/local/include -I. \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --go_out=plugins=grpc,paths=source_relative:. \ + hello.proto diff --git a/proxy/testdata/hello.pb.go b/proxy/testdata/hello.pb.go new file mode 100644 index 0000000..2ea4116 --- /dev/null +++ b/proxy/testdata/hello.pb.go @@ -0,0 +1,194 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: hello.proto + +package proxy_test + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_61ef911816e0a8ce, []int{0} +} + +func (m *HelloRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HelloRequest.Unmarshal(m, b) +} +func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic) +} +func (m *HelloRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_HelloRequest.Merge(m, src) +} +func (m *HelloRequest) XXX_Size() int { + return xxx_messageInfo_HelloRequest.Size(m) +} +func (m *HelloRequest) XXX_DiscardUnknown() { + xxx_messageInfo_HelloRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_HelloRequest proto.InternalMessageInfo + +func (m *HelloRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +type HelloReply struct { + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HelloReply) Reset() { *m = HelloReply{} } +func (m *HelloReply) String() string { return proto.CompactTextString(m) } +func (*HelloReply) ProtoMessage() {} +func (*HelloReply) Descriptor() ([]byte, []int) { + return fileDescriptor_61ef911816e0a8ce, []int{1} +} + +func (m *HelloReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HelloReply.Unmarshal(m, b) +} +func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic) +} +func (m *HelloReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_HelloReply.Merge(m, src) +} +func (m *HelloReply) XXX_Size() int { + return xxx_messageInfo_HelloReply.Size(m) +} +func (m *HelloReply) XXX_DiscardUnknown() { + xxx_messageInfo_HelloReply.DiscardUnknown(m) +} + +var xxx_messageInfo_HelloReply proto.InternalMessageInfo + +func (m *HelloReply) GetMessage() string { + if m != nil { + return m.Message + } + return "" +} + +func init() { + proto.RegisterType((*HelloRequest)(nil), "proxy_test.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "proxy_test.HelloReply") +} + +func init() { proto.RegisterFile("hello.proto", fileDescriptor_61ef911816e0a8ce) } + +var fileDescriptor_61ef911816e0a8ce = []byte{ + // 145 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0x48, 0xcd, 0xc9, + 0xc9, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2a, 0x28, 0xca, 0xaf, 0xa8, 0x8c, 0x2f, + 0x49, 0x2d, 0x2e, 0x51, 0x52, 0xe2, 0xe2, 0xf1, 0x00, 0x49, 0x05, 0xa5, 0x16, 0x96, 0xa6, 0x16, + 0x97, 0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, + 0x81, 0xd9, 0x4a, 0x6a, 0x5c, 0x5c, 0x50, 0x35, 0x05, 0x39, 0x95, 0x42, 0x12, 0x5c, 0xec, 0xb9, + 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0x30, 0x45, 0x30, 0xae, 0x91, 0x27, 0x17, 0xbb, 0x7b, 0x51, 0x6a, + 0x6a, 0x49, 0x6a, 0x91, 0x90, 0x1d, 0x17, 0x47, 0x70, 0x62, 0x25, 0x58, 0x97, 0x90, 0x84, 0x1e, + 0xc2, 0x3e, 0x3d, 0x64, 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x24, 0xb1, + 0x81, 0x5d, 0x6a, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x7f, 0xe6, 0xbe, 0xb8, 0x00, 0x00, + 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// GreeterClient is the client API for Greeter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type GreeterClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc *grpc.ClientConn +} + +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/proxy_test.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GreeterServer is the server API for Greeter service. +type GreeterServer interface { + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { + s.RegisterService(&_Greeter_serviceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proxy_test.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Greeter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "proxy_test.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "hello.proto", +} diff --git a/proxy/testdata/hello.proto b/proxy/testdata/hello.proto new file mode 100644 index 0000000..4ee0771 --- /dev/null +++ b/proxy/testdata/hello.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package proxy_test; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} From 8f4dfc5d57271a937d76612455bc3242fd2c1cfb Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 1 Nov 2019 16:03:51 +0100 Subject: [PATCH 3/4] auth+macaroons: allow authorization by macaroon only --- auth/authenticator.go | 140 +++++++++++++++++++++++++++------- auth/authenticator_test.go | 150 +++++++++++++++++++++++++++++++++++++ macaroons/service.go | 80 +++++++++++++------- 3 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 auth/authenticator_test.go 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 From aef413da0adb45a7bfd0b92c9222241ef8b39a03 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2019 16:58:19 +0100 Subject: [PATCH 4/4] auth+proxy: forward auth to backend --- auth/authenticator.go | 26 ++++++++++++++++++++++---- proxy/proxy.go | 17 +++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/auth/authenticator.go b/auth/authenticator.go index be91a1f..e7d2123 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -29,6 +29,7 @@ const ( var ( authRegex = regexp.MustCompile("LSAT (.*?):([a-f0-9]{64})") + authFormat = "LSAT %s:%s" opWildcard = "*" ) @@ -65,7 +66,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 := authFromHeader(header) + mac, preimageBytes, err := FromHeader(header) if err != nil { log.Debugf("Deny: %v", err) return false @@ -125,7 +126,7 @@ func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( return header, nil } -// authFromHeader tries to extract authentication information from HTTP headers. +// FromHeader 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 : @@ -133,7 +134,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 authFromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) { +func FromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) { var authHeader string switch { @@ -165,7 +166,7 @@ func authFromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) { mac := &macaroon.Macaroon{} err = mac.UnmarshalBinary(macBytes) if err != nil { - return nil, nil, fmt.Errorf("unable to unmarshal " + + return nil, nil, fmt.Errorf("unable to unmarshal "+ "macaroon: %v", err) } preimageBytes, err := hex.DecodeString(preimageHex) @@ -216,3 +217,20 @@ func authFromHeader(header *http.Header) (*macaroon.Macaroon, []byte, error) { return mac, preimageBytes, 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 { + + macBytes, err := mac.MarshalBinary() + if err != nil { + return err + } + value := fmt.Sprintf( + authFormat, base64.StdEncoding.EncodeToString(macBytes), + hex.EncodeToString(preimage), + ) + header.Set(HeaderAuthorization, value) + return nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go index f2749d0..c1c30be 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -19,7 +19,7 @@ const ( // formatPattern is the pattern in which the request log will be // printed. This is loosely oriented on the apache log format. // An example entry would look like this: - // 2019-11-09 04:07:55.072 [INF] PRXY: 66.249.69.89 - - + // 2019-11-09 04:07:55.072 [INF] PRXY: 66.249.69.89 - - // "GET /availability/v1/btc.json HTTP/1.1" "" "Mozilla/5.0 ..." formatPattern = "- - \"%s %s %s\" \"%s\" \"%s\"" hdrContentType = "Content-Type" @@ -181,9 +181,18 @@ func (p *Proxy) director(req *http.Request) { req.URL.Host = target.Address req.URL.Scheme = target.Protocol - // Don't forward the authorization header since the - // services won't know what it is. - req.Header.Del("Authorization") + // Make sure we always forward the authorization in the correct/ + // default format so the backend knows what to do with it. + mac, preimage, err := auth.FromHeader(&req.Header) + if err == nil { + // It could be that there is no auth information because + // none is needed for this particular request. So we + // only continue if no error is set. + err := auth.SetHeader(&req.Header, mac, preimage) + if err != nil { + log.Errorf("could not set header: %v", err) + } + } // Now overwrite header fields of the client request // with the fields from the configuration file.