Skip to content

Commit 2ada1f1

Browse files
committed
itest: add gRPC certificate and auth tests
1 parent 08aa091 commit 2ada1f1

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

itest/litd_mode_integrated_test.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,125 @@ package itest
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"encoding/hex"
8+
"fmt"
9+
"io/ioutil"
10+
"testing"
511

612
"github.com/btcsuite/btcutil"
13+
"github.com/lightninglabs/faraday/frdrpc"
14+
"github.com/lightninglabs/loop/looprpc"
15+
"github.com/lightninglabs/pool/poolrpc"
716
"github.com/lightningnetwork/lnd/lnrpc"
817
"github.com/stretchr/testify/require"
18+
"golang.org/x/net/http2"
19+
"google.golang.org/grpc"
20+
"google.golang.org/grpc/credentials"
21+
"google.golang.org/grpc/metadata"
22+
"google.golang.org/protobuf/encoding/protojson"
23+
"google.golang.org/protobuf/proto"
24+
"gopkg.in/macaroon.v2"
25+
)
26+
27+
// requestFn is a function type for a helper function that makes a daemon
28+
// specific request and returns the response and error for it. This is used to
29+
// abstract away the lnd/faraday/loop/pool specific gRPC code from the actual
30+
// test code.
31+
type requestFn func(ctx context.Context,
32+
c grpc.ClientConnInterface) (proto.Message, error)
33+
34+
// macaroonFn is a function that returns the correct macaroon path for each of
35+
// the integrated daemons.
36+
type macaroonFn func(cfg *LitNodeConfig) string
37+
38+
var (
39+
dummyMac = makeMac()
40+
dummyMacBytes = serializeMac(dummyMac)
41+
42+
marshalOptions = &protojson.MarshalOptions{
43+
UseProtoNames: true,
44+
EmitUnpopulated: true,
45+
}
46+
47+
transport = &http2.Transport{
48+
TLSClientConfig: &tls.Config{
49+
InsecureSkipVerify: true,
50+
},
51+
}
52+
53+
lndRequestFn = func(ctx context.Context,
54+
c grpc.ClientConnInterface) (proto.Message, error) {
55+
56+
lndConn := lnrpc.NewLightningClient(c)
57+
return lndConn.GetInfo(
58+
ctx, &lnrpc.GetInfoRequest{},
59+
)
60+
}
61+
lndMacaroonFn = func(cfg *LitNodeConfig) string {
62+
return cfg.AdminMacPath
63+
}
64+
faradayRequestFn = func(ctx context.Context,
65+
c grpc.ClientConnInterface) (proto.Message, error) {
66+
67+
frdConn := frdrpc.NewFaradayServerClient(c)
68+
return frdConn.RevenueReport(
69+
ctx, &frdrpc.RevenueReportRequest{},
70+
)
71+
}
72+
faradayMacaroonFn = func(cfg *LitNodeConfig) string {
73+
return cfg.FaradayMacPath
74+
}
75+
loopRequestFn = func(ctx context.Context,
76+
c grpc.ClientConnInterface) (proto.Message, error) {
77+
78+
loopConn := looprpc.NewSwapClientClient(c)
79+
return loopConn.ListSwaps(
80+
ctx, &looprpc.ListSwapsRequest{},
81+
)
82+
}
83+
loopMacaroonFn = func(cfg *LitNodeConfig) string {
84+
return cfg.LoopMacPath
85+
}
86+
poolRequestFn = func(ctx context.Context,
87+
c grpc.ClientConnInterface) (proto.Message, error) {
88+
89+
poolConn := poolrpc.NewTraderClient(c)
90+
return poolConn.GetInfo(
91+
ctx, &poolrpc.GetInfoRequest{},
92+
)
93+
}
94+
poolMacaroonFn = func(cfg *LitNodeConfig) string {
95+
return cfg.PoolMacPath
96+
}
97+
98+
endpoints = []struct {
99+
name string
100+
macaroonFn macaroonFn
101+
requestFn requestFn
102+
successPattern string
103+
}{{
104+
name: "lnrpc",
105+
macaroonFn: lndMacaroonFn,
106+
requestFn: lndRequestFn,
107+
successPattern: "\"identity_pubkey\":\"0",
108+
}, {
109+
name: "frdrpc",
110+
macaroonFn: faradayMacaroonFn,
111+
requestFn: faradayRequestFn,
112+
successPattern: "\"reports\":[]",
113+
}, {
114+
name: "looprpc",
115+
macaroonFn: loopMacaroonFn,
116+
requestFn: loopRequestFn,
117+
successPattern: "\"swaps\":[]",
118+
}, {
119+
name: "poolrpc",
120+
macaroonFn: poolMacaroonFn,
121+
requestFn: poolRequestFn,
122+
successPattern: "\"accounts_active\":0",
123+
}}
9124
)
10125

11126
// testModeIntegrated makes sure that in integrated mode all daemons work
@@ -21,4 +136,157 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) {
21136
resp, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{})
22137
require.NoError(t.t, err)
23138
require.NotEmpty(t.t, resp.Alias)
139+
require.Contains(t.t, resp.Alias, "0")
140+
141+
t.t.Run("certificate check", func(tt *testing.T) {
142+
runCertificateCheck(tt, net.Alice)
143+
})
144+
t.t.Run("gRPC macaroon auth check", func(tt *testing.T) {
145+
cfg := net.Alice.Cfg
146+
147+
for _, endpoint := range endpoints {
148+
endpoint := endpoint
149+
tt.Run(endpoint.name+" lnd port", func(ttt *testing.T) {
150+
runGRPCAuthTest(
151+
ttt, cfg.RPCAddr(), cfg.TLSCertPath,
152+
endpoint.macaroonFn(cfg),
153+
endpoint.requestFn,
154+
endpoint.successPattern,
155+
)
156+
})
157+
158+
tt.Run(endpoint.name+" lit port", func(ttt *testing.T) {
159+
runGRPCAuthTest(
160+
ttt, cfg.LitAddr(), cfg.TLSCertPath,
161+
endpoint.macaroonFn(cfg),
162+
endpoint.requestFn,
163+
endpoint.successPattern,
164+
)
165+
})
166+
}
167+
})
168+
}
169+
170+
// runCertificateCheck checks that the TLS certificates presented to clients are
171+
// what we expect them to be.
172+
func runCertificateCheck(t *testing.T, node *HarnessNode) {
173+
// In integrated mode we expect the LiT HTTPS port (8443 by default) and
174+
// lnd's RPC port to present the same certificate, namely lnd's TLS
175+
// cert.
176+
litCerts, err := getServerCertificates(node.Cfg.LitAddr())
177+
require.NoError(t, err)
178+
require.Len(t, litCerts, 1)
179+
require.Equal(
180+
t, "lnd autogenerated cert", litCerts[0].Issuer.Organization[0],
181+
)
182+
183+
lndCerts, err := getServerCertificates(node.Cfg.RPCAddr())
184+
require.NoError(t, err)
185+
require.Len(t, lndCerts, 1)
186+
require.Equal(
187+
t, "lnd autogenerated cert", lndCerts[0].Issuer.Organization[0],
188+
)
189+
190+
require.Equal(t, litCerts[0].Raw, lndCerts[0].Raw)
191+
}
192+
193+
// runGRPCAuthTest tests authentication of the given gRPC interface.
194+
func runGRPCAuthTest(t *testing.T, hostPort, tlsCertPath, macPath string,
195+
makeRequest requestFn, successContent string) {
196+
197+
ctxb := context.Background()
198+
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
199+
defer cancel()
200+
201+
rawConn, err := connectRPC(ctxt, hostPort, tlsCertPath)
202+
require.NoError(t, err)
203+
204+
// We have a connection without any macaroon. A call should fail.
205+
_, err = makeRequest(ctxt, rawConn)
206+
require.Error(t, err)
207+
require.Contains(t, err.Error(), "expected 1 macaroon, got 0")
208+
209+
// Add dummy data as the macaroon, that should fail as well.
210+
ctxm := macaroonContext(ctxt, []byte("dummy"))
211+
_, err = makeRequest(ctxm, rawConn)
212+
require.Error(t, err)
213+
require.Contains(t, err.Error(), "packet too short")
214+
215+
// Add a macaroon that can be parsed but that's not issued by lnd, which
216+
// should also fail.
217+
ctxm = macaroonContext(ctxt, dummyMacBytes)
218+
_, err = makeRequest(ctxm, rawConn)
219+
require.Error(t, err)
220+
require.Contains(t, err.Error(), "cannot get macaroon: root key with")
221+
222+
// Then finally we try with the correct macaroon which should now
223+
// succeed.
224+
macBytes, err := ioutil.ReadFile(macPath)
225+
require.NoError(t, err)
226+
ctxm = macaroonContext(ctxt, macBytes)
227+
resp, err := makeRequest(ctxm, rawConn)
228+
require.NoError(t, err)
229+
230+
json, err := marshalOptions.Marshal(resp)
231+
require.NoError(t, err)
232+
require.Contains(t, string(json), successContent)
233+
}
234+
235+
// getServerCertificates returns the TLS certificates that a server presents to
236+
// clients.
237+
func getServerCertificates(hostPort string) ([]*x509.Certificate, error) {
238+
// We don't care about the validity of the certificate, we just want to
239+
// download it.
240+
conn, err := tls.Dial("tcp", hostPort, transport.TLSClientConfig)
241+
if err != nil {
242+
return nil, fmt.Errorf("error dialing %s: %v", hostPort, err)
243+
}
244+
defer func() {
245+
_ = conn.Close()
246+
}()
247+
248+
return conn.ConnectionState().PeerCertificates, nil
249+
}
250+
251+
func macaroonContext(ctx context.Context, macBytes []byte) context.Context {
252+
md := metadata.MD{}
253+
if len(macBytes) > 0 {
254+
md["macaroon"] = []string{hex.EncodeToString(macBytes)}
255+
}
256+
return metadata.NewOutgoingContext(ctx, md)
257+
}
258+
259+
func makeMac() *macaroon.Macaroon {
260+
dummyMac, err := macaroon.New(
261+
[]byte("aabbccddeeff00112233445566778899"), []byte("AA=="),
262+
"LSAT", macaroon.LatestVersion,
263+
)
264+
if err != nil {
265+
panic(fmt.Errorf("unable to create macaroon: %v", err))
266+
}
267+
return dummyMac
268+
}
269+
270+
func serializeMac(mac *macaroon.Macaroon) []byte {
271+
macBytes, err := mac.MarshalBinary()
272+
if err != nil {
273+
panic(fmt.Errorf("unable to serialize macaroon: %v", err))
274+
}
275+
return macBytes
276+
}
277+
278+
func connectRPC(ctx context.Context, hostPort,
279+
tlsCertPath string) (*grpc.ClientConn, error) {
280+
281+
tlsCreds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
282+
if err != nil {
283+
return nil, err
284+
}
285+
286+
opts := []grpc.DialOption{
287+
grpc.WithBlock(),
288+
grpc.WithTransportCredentials(tlsCreds),
289+
}
290+
291+
return grpc.DialContext(ctx, hostPort, opts...)
24292
}

0 commit comments

Comments
 (0)