firewall: intercept with privacy flags

gRPC message interception depends now on privacy flags.
This commit is contained in:
bitromortac 2023-11-14 15:04:46 +01:00
parent c961472789
commit 07bcfe19c0
No known key found for this signature in database
GPG key ID: 1965063FC13BEBE2
2 changed files with 308 additions and 187 deletions

View file

@ -110,12 +110,6 @@ func (p *PrivacyMapper) Intercept(ctx context.Context,
return nil, fmt.Errorf("could not extract ID from macaroon")
}
// Get group ID for session ID.
groupID, err := p.sessionDB.GetGroupID(sessionID)
if err != nil {
return nil, err
}
log.Tracef("PrivacyMapper: Intercepting %v", ri)
switch r := req.InterceptType.(type) {
@ -133,7 +127,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context,
}
replacement, err := p.checkAndReplaceIncomingRequest(
ctx, r.Request.MethodFullUri, msg, groupID,
ctx, r.Request.MethodFullUri, msg, sessionID,
)
if err != nil {
return mid.RPCErr(req, err)
@ -167,7 +161,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context,
}
replacement, err := p.replaceOutgoingResponse(
ctx, r.Response.MethodFullUri, msg, groupID,
ctx, r.Response.MethodFullUri, msg, sessionID,
)
if err != nil {
return mid.RPCErr(req, err)
@ -192,14 +186,19 @@ func (p *PrivacyMapper) Intercept(ctx context.Context,
// checkAndReplaceIncomingRequest inspects an incoming request and optionally
// modifies some of the request parameters.
func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context,
uri string, req proto.Message, groupID session.ID) (proto.Message,
uri string, req proto.Message, sessionID session.ID) (proto.Message,
error) {
db := p.newDB(groupID)
session, err := p.sessionDB.GetSessionByID(sessionID)
if err != nil {
return nil, err
}
db := p.newDB(session.GroupID)
// If we don't have a handler for the URI, we don't allow the request
// to go through.
checker, ok := p.checkers(db)[uri]
checker, ok := p.checkers(db, session.PrivacyFlags)[uri]
if !ok {
return nil, ErrNotSupportedByPrivacyMapper
}
@ -218,13 +217,18 @@ func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context,
// replaceOutgoingResponse inspects the responses before sending them out to the
// client and replaces them if needed.
func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string,
resp proto.Message, groupID session.ID) (proto.Message, error) {
resp proto.Message, sessionID session.ID) (proto.Message, error) {
db := p.newDB(groupID)
session, err := p.sessionDB.GetSessionByID(sessionID)
if err != nil {
return nil, err
}
db := p.newDB(session.GroupID)
// If we don't have a handler for the URI, we don't allow the response
// to go to avoid accidental leaks.
checker, ok := p.checkers(db)[uri]
checker, ok := p.checkers(db, session.PrivacyFlags)[uri]
if !ok {
return nil, ErrNotSupportedByPrivacyMapper
}
@ -240,64 +244,66 @@ func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string,
return checker.HandleResponse(ctx, resp)
}
func (p *PrivacyMapper) checkers(
db firewalldb.PrivacyMapDB) map[string]mid.RoundTripChecker {
func (p *PrivacyMapper) checkers(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) map[string]mid.RoundTripChecker {
return map[string]mid.RoundTripChecker{
"/lnrpc.Lightning/GetInfo": mid.NewResponseRewriter(
&lnrpc.GetInfoRequest{}, &lnrpc.GetInfoResponse{},
handleGetInfoResponse(db), mid.PassThroughErrorHandler,
handleGetInfoResponse(db, flags),
mid.PassThroughErrorHandler,
),
"/lnrpc.Lightning/ForwardingHistory": mid.NewResponseRewriter(
&lnrpc.ForwardingHistoryRequest{},
&lnrpc.ForwardingHistoryResponse{},
handleFwdHistoryResponse(db, p.randIntn),
handleFwdHistoryResponse(db, flags, p.randIntn),
mid.PassThroughErrorHandler,
),
"/lnrpc.Lightning/FeeReport": mid.NewResponseRewriter(
&lnrpc.FeeReportRequest{}, &lnrpc.FeeReportResponse{},
handleFeeReportResponse(db),
handleFeeReportResponse(db, flags),
mid.PassThroughErrorHandler,
),
"/lnrpc.Lightning/ListChannels": mid.NewFullRewriter(
&lnrpc.ListChannelsRequest{},
&lnrpc.ListChannelsResponse{},
handleListChannelsRequest(db),
handleListChannelsResponse(db, p.randIntn),
handleListChannelsRequest(db, flags),
handleListChannelsResponse(db, flags, p.randIntn),
mid.PassThroughErrorHandler,
),
"/lnrpc.Lightning/UpdateChannelPolicy": mid.NewFullRewriter(
&lnrpc.PolicyUpdateRequest{},
&lnrpc.PolicyUpdateResponse{},
handleUpdatePolicyRequest(db),
handleUpdatePolicyResponse(db),
handleUpdatePolicyRequest(db, flags),
handleUpdatePolicyResponse(db, flags),
mid.PassThroughErrorHandler,
),
}
}
func handleGetInfoResponse(db firewalldb.PrivacyMapDB) func(ctx context.Context,
func handleGetInfoResponse(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) func(ctx context.Context,
r *lnrpc.GetInfoResponse) (proto.Message, error) {
return func(ctx context.Context, r *lnrpc.GetInfoResponse) (
return func(_ context.Context, r *lnrpc.GetInfoResponse) (
proto.Message, error) {
var pseudoPubKey string
err := db.Update(
func(tx firewalldb.PrivacyMapTx) error {
var err error
pseudoPubKey, err = firewalldb.HideString(
tx, r.IdentityPubkey,
)
if err != nil {
return err
}
// We hide the pubkey unless it is disabled.
pseudoPubKey := r.IdentityPubkey
if !flags.Contains(session.ClearPubkeys) {
err := db.Update(
func(tx firewalldb.PrivacyMapTx) error {
var err error
pseudoPubKey, err = firewalldb.HideString(
tx, r.IdentityPubkey,
)
return nil
},
)
if err != nil {
return nil, err
return err
},
)
if err != nil {
return nil, err
}
}
return &lnrpc.GetInfoResponse{
@ -327,6 +333,7 @@ func handleGetInfoResponse(db firewalldb.PrivacyMapDB) func(ctx context.Context,
}
func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags,
randIntn func(int) (int, error)) func(ctx context.Context,
r *lnrpc.ForwardingHistoryResponse) (proto.Message, error) {
@ -339,36 +346,48 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB,
err := db.Update(func(tx firewalldb.PrivacyMapTx) error {
for i, fe := range r.ForwardingEvents {
// Deterministically hide channel ids.
chanIn, err := firewalldb.HideUint64(
tx, fe.ChanIdIn,
)
if err != nil {
return err
var err error
chanIn := fe.ChanIdIn
chanOut := fe.ChanIdOut
if !flags.Contains(session.ClearChanIDs) {
// Deterministically hide channel ids.
chanIn, err = firewalldb.HideUint64(
tx, chanIn,
)
if err != nil {
return err
}
chanOut, err = firewalldb.HideUint64(
tx, chanOut,
)
if err != nil {
return err
}
}
chanOut, err := firewalldb.HideUint64(
tx, fe.ChanIdOut,
)
if err != nil {
return err
}
amtOutMsat := fe.AmtOutMsat
feeMsat := fe.FeeMsat
if !flags.Contains(session.ClearAmounts) {
// We randomize the outgoing amount for
// privacy.
amtOutMsat, err = hideAmount(
randIntn, amountVariation,
amtOutMsat,
)
if err != nil {
return err
}
// We randomize the outgoing amount for privacy.
amtOutMsat, err := hideAmount(
randIntn, amountVariation,
fe.AmtOutMsat,
)
if err != nil {
return err
}
// We randomize fees for privacy.
feeMsat, err := hideAmount(
randIntn, amountVariation, fe.FeeMsat,
)
if err != nil {
return err
// We randomize fees for privacy.
feeMsat, err = hideAmount(
randIntn, amountVariation,
feeMsat,
)
if err != nil {
return err
}
}
// Populate other fields in a consistent manner.
@ -377,13 +396,16 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB,
amtIn := amtInMsat / 1000
fee := feeMsat / 1000
// We randomize the forwarding timestamp.
timestamp, err := hideTimestamp(
randIntn, timeVariation,
time.Unix(0, int64(fe.TimestampNs)),
)
if err != nil {
return err
timestamp := time.Unix(0, int64(fe.TimestampNs))
if !flags.Contains(session.ClearTimeStamps) {
// We randomize the forwarding timestamp.
timestamp, err = hideTimestamp(
randIntn, timeVariation,
timestamp,
)
if err != nil {
return err
}
}
fwdEvents[i] = &lnrpc.ForwardingEvent{
@ -416,9 +438,9 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB,
}
}
func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func(
ctx context.Context, r *lnrpc.FeeReportResponse) (proto.Message,
error) {
func handleFeeReportResponse(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) func(ctx context.Context,
r *lnrpc.FeeReportResponse) (proto.Message, error) {
return func(ctx context.Context, r *lnrpc.FeeReportResponse) (
proto.Message, error) {
@ -426,19 +448,27 @@ func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func(
chanFees := make([]*lnrpc.ChannelFeeReport, len(r.ChannelFees))
err := db.Update(func(tx firewalldb.PrivacyMapTx) error {
var err error
for i, c := range r.ChannelFees {
chanID, err := firewalldb.HideUint64(
tx, c.ChanId,
)
if err != nil {
return err
chanID := c.ChanId
if !flags.Contains(session.ClearChanIDs) {
chanID, err = firewalldb.HideUint64(
tx, chanID,
)
if err != nil {
return err
}
}
chanPoint, err := firewalldb.HideChanPointStr(
tx, c.ChannelPoint,
)
if err != nil {
return err
chanPoint := c.ChannelPoint
if !flags.Contains(session.ClearChanIDs) {
chanPoint, err = firewalldb.HideChanPointStr(
tx, chanPoint,
)
if err != nil {
return err
}
}
chanFees[i] = &lnrpc.ChannelFeeReport{
@ -465,9 +495,9 @@ func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func(
}
}
func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func(
ctx context.Context, r *lnrpc.ListChannelsRequest) (proto.Message,
error) {
func handleListChannelsRequest(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) func(ctx context.Context,
r *lnrpc.ListChannelsRequest) (proto.Message, error) {
return func(ctx context.Context, r *lnrpc.ListChannelsRequest) (
proto.Message, error) {
@ -476,6 +506,10 @@ func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func(
return nil, nil
}
if flags.Contains(session.ClearPubkeys) {
return r, nil
}
err := db.View(func(tx firewalldb.PrivacyMapTx) error {
peer, err := firewalldb.RevealBytes(tx, r.Peer)
if err != nil {
@ -494,6 +528,7 @@ func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func(
}
func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags,
randIntn func(int) (int, error)) func(ctx context.Context,
r *lnrpc.ListChannelsResponse) (proto.Message, error) {
@ -501,47 +536,66 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
proto.Message, error) {
hideAmount := func(a int64) (int64, error) {
hiddenAmount, err := hideAmount(
randIntn, amountVariation, uint64(a),
)
if err != nil {
return 0, err
if !flags.Contains(session.ClearAmounts) {
hiddenAmount, err := hideAmount(
randIntn, amountVariation, uint64(a),
)
if err != nil {
return 0, err
}
return int64(hiddenAmount), nil
}
return int64(hiddenAmount), nil
return a, nil
}
hidePubkeys := !flags.Contains(session.ClearPubkeys)
hideChanIds := !flags.Contains(session.ClearChanIDs)
channels := make([]*lnrpc.Channel, len(r.Channels))
err := db.Update(func(tx firewalldb.PrivacyMapTx) error {
for i, c := range r.Channels {
// Deterministically hide the peer pubkey,
// the channel point, and the channel id.
remotePub, err := firewalldb.HideString(
tx, c.RemotePubkey,
)
if err != nil {
return err
var err error
// We hide the remote pubkey unless it is
// disabled.
remotePub := c.RemotePubkey
if hidePubkeys {
remotePub, err = firewalldb.HideString(
tx, c.RemotePubkey,
)
if err != nil {
return err
}
}
chanPoint, err := firewalldb.HideChanPointStr(
tx, c.ChannelPoint,
)
if err != nil {
return err
}
chanPoint := c.ChannelPoint
chanID := c.ChanId
if hideChanIds {
chanPoint, err = firewalldb.HideChanPointStr(
tx, c.ChannelPoint,
)
if err != nil {
return err
}
chanID, err := firewalldb.HideUint64(
tx, c.ChanId,
)
if err != nil {
return err
chanID, err = firewalldb.HideUint64(
tx, c.ChanId,
)
if err != nil {
return err
}
}
// We hide the initiator.
initiator, err := hideBool(randIntn)
if err != nil {
return err
initiator := c.Initiator
if !flags.Contains(session.ClearChanInitiator) {
initiator, err = hideBool(randIntn)
if err != nil {
return err
}
}
// Consider the capacity to be public
@ -563,8 +617,12 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
localBalance = c.Capacity
}
// We adapt the remote balance accordingly.
remoteBalance := c.Capacity - localBalance
remoteBalance := c.RemoteBalance
if !flags.Contains(session.ClearAmounts) {
// We adapt the remote balance
// accordingly.
remoteBalance = c.Capacity - localBalance
}
// We hide the total sats sent and received.
satsReceived, err := hideAmount(
@ -587,6 +645,11 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
[]*lnrpc.HTLC, len(c.PendingHtlcs),
)
// Only show the HTLCs if the flag is set.
if flags.Contains(session.ClearHTLCs) {
copy(pendingHtlcs, c.PendingHtlcs)
}
// We hide the unsettled balance.
unsettled, err := hideAmount(c.UnsettledBalance)
if err != nil {
@ -648,11 +711,11 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
}
}
func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func(
ctx context.Context, r *lnrpc.PolicyUpdateRequest) (proto.Message,
error) {
func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) func(ctx context.Context,
r *lnrpc.PolicyUpdateRequest) (proto.Message, error) {
return func(ctx context.Context, r *lnrpc.PolicyUpdateRequest) (
return func(_ context.Context, r *lnrpc.PolicyUpdateRequest) (
proto.Message, error) {
chanPoint := r.GetChanPoint()
@ -668,21 +731,19 @@ func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func(
return nil, err
}
index := chanPoint.GetOutputIndex()
var (
newTxid string
newIndex uint32
)
err = db.View(func(tx firewalldb.PrivacyMapTx) error {
var err error
newTxid, newIndex, err = firewalldb.RevealChanPoint(
tx, txid.String(), index,
)
return err
})
if err != nil {
return nil, err
newTxid := txid.String()
newIndex := chanPoint.GetOutputIndex()
if !flags.Contains(session.ClearChanIDs) {
err = db.View(func(tx firewalldb.PrivacyMapTx) error {
var err error
newTxid, newIndex, err = firewalldb.RevealChanPoint(
tx, newTxid, newIndex,
)
return err
})
if err != nil {
return nil, err
}
}
r.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
@ -698,13 +759,17 @@ func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func(
}
}
func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB) func(
ctx context.Context, r *lnrpc.PolicyUpdateResponse) (proto.Message,
error) {
func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB,
flags session.PrivacyFlags) func(ctx context.Context,
r *lnrpc.PolicyUpdateResponse) (proto.Message, error) {
return func(ctx context.Context, r *lnrpc.PolicyUpdateResponse) (
return func(_ context.Context, r *lnrpc.PolicyUpdateResponse) (
proto.Message, error) {
if flags.Contains(session.ClearChanIDs) {
return r, nil
}
failedUpdates := make(
[]*lnrpc.FailedUpdate, len(r.FailedUpdates),
)

View file

@ -19,8 +19,58 @@ import (
// TestPrivacyMapper tests that the PrivacyMapper correctly intercepts specific
// RPC calls.
func TestPrivacyMapper(t *testing.T) {
var (
clearForwarding = &lnrpc.ForwardingHistoryResponse{
ForwardingEvents: []*lnrpc.ForwardingEvent{
{
AmtIn: 2_000,
AmtInMsat: 2_000_000,
AmtOut: 1_000,
AmtOutMsat: 1_000_000,
Fee: 1_000,
FeeMsat: 1_000_000,
Timestamp: 1_000,
TimestampNs: 1_000_000_000_000,
ChanIdIn: 123,
ChanIdOut: 321,
},
{
AmtIn: 3_000,
AmtInMsat: 3_000_000,
AmtOut: 2_000,
AmtOutMsat: 2_000_000,
Fee: 1_000,
FeeMsat: 1_000_000,
Timestamp: 1_000,
TimestampNs: 1_000_000_000_000,
ChanIdIn: 678,
ChanIdOut: 876,
},
},
}
clearListChannel = &lnrpc.ListChannelsResponse{
Channels: []*lnrpc.Channel{
{
Capacity: 1_000_000,
RemoteBalance: 600_000,
LocalBalance: 499_000,
CommitFee: 1_000,
TotalSatoshisSent: 500_000,
TotalSatoshisReceived: 450_000,
RemotePubkey: "01020304",
Initiator: false,
ChanId: 123,
ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0",
PendingHtlcs: []*lnrpc.HTLC{{HashLock: []byte("aaaa")}, {HashLock: []byte("bbbb")}},
},
},
}
)
tests := []struct {
name string
privacyFlags session.PrivacyFlags
uri string
msgType rpcperms.InterceptType
msg proto.Message
@ -42,38 +92,42 @@ func TestPrivacyMapper(t *testing.T) {
IdentityPubkey: "a44ef01c3bff970ef495c",
},
},
{
name: "GetInfo Response clear pubkey",
uri: "/lnrpc.Lightning/GetInfo",
msgType: rpcperms.TypeResponse,
privacyFlags: session.PrivacyFlags{
session.ClearPubkeys,
},
msg: &lnrpc.GetInfoResponse{
Alias: "Tinker Bell",
IdentityPubkey: "Tinker Bell's pub key",
Uris: []string{
"Neverland 1",
"Neverland 2",
},
},
expectedReplacement: &lnrpc.GetInfoResponse{
IdentityPubkey: "Tinker Bell's pub key",
},
},
{
name: "ForwardingHistory Response clear",
uri: "/lnrpc.Lightning/ForwardingHistory",
privacyFlags: []session.PrivacyFlag{
session.ClearChanIDs,
session.ClearAmounts,
session.ClearTimeStamps,
},
msgType: rpcperms.TypeResponse,
msg: clearForwarding,
expectedReplacement: clearForwarding,
},
{
name: "ForwardingHistory Response",
uri: "/lnrpc.Lightning/ForwardingHistory",
msgType: rpcperms.TypeResponse,
msg: &lnrpc.ForwardingHistoryResponse{
ForwardingEvents: []*lnrpc.ForwardingEvent{
{
AmtIn: 2_000,
AmtInMsat: 2_000_000,
AmtOut: 1_000,
AmtOutMsat: 1_000_000,
Fee: 1_000,
FeeMsat: 1_000_000,
Timestamp: 1_000,
TimestampNs: 1_000_000_000_000,
ChanIdIn: 123,
ChanIdOut: 321,
},
{
AmtIn: 3_000,
AmtInMsat: 3_000_000,
AmtOut: 2_000,
AmtOutMsat: 2_000_000,
Fee: 1_000,
FeeMsat: 1_000_000,
Timestamp: 1_000,
TimestampNs: 1_000_000_000_000,
ChanIdIn: 678,
ChanIdOut: 876,
},
},
},
msg: clearForwarding,
expectedReplacement: &lnrpc.ForwardingHistoryResponse{
ForwardingEvents: []*lnrpc.ForwardingEvent{
{
@ -147,23 +201,7 @@ func TestPrivacyMapper(t *testing.T) {
name: "ListChannels Response",
uri: "/lnrpc.Lightning/ListChannels",
msgType: rpcperms.TypeResponse,
msg: &lnrpc.ListChannelsResponse{
Channels: []*lnrpc.Channel{
{
Capacity: 1_000_000,
RemoteBalance: 600_000,
LocalBalance: 499_000,
CommitFee: 1_000,
TotalSatoshisSent: 500_000,
TotalSatoshisReceived: 450_000,
RemotePubkey: "01020304",
Initiator: false,
ChanId: 123,
ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0",
PendingHtlcs: []*lnrpc.HTLC{{HashLock: []byte("aaaa")}, {HashLock: []byte("bbbb")}},
},
},
},
msg: clearListChannel,
expectedReplacement: &lnrpc.ListChannelsResponse{
Channels: []*lnrpc.Channel{
{
@ -182,6 +220,20 @@ func TestPrivacyMapper(t *testing.T) {
},
},
},
{
name: "ListChannels Response clear",
privacyFlags: []session.PrivacyFlag{
session.ClearPubkeys,
session.ClearChanIDs,
session.ClearAmounts,
session.ClearHTLCs,
session.ClearChanInitiator,
},
uri: "/lnrpc.Lightning/ListChannels",
msgType: rpcperms.TypeResponse,
msg: clearListChannel,
expectedReplacement: clearListChannel,
},
{
name: "UpdateChannelPolicy Request txid string",
uri: "/lnrpc.Lightning/UpdateChannelPolicy",
@ -298,6 +350,8 @@ func TestPrivacyMapper(t *testing.T) {
pd := firewalldb.NewMockSessionDB()
pd.AddPair(sessionID, sessionID)
err = pd.AddPrivacyFlags(sessionID, test.privacyFlags)
require.NoError(t, err)
// randIntn is used for deterministic testing.
randIntn := func(n int) (int, error) { return 100, nil }
@ -346,6 +400,8 @@ func TestPrivacyMapper(t *testing.T) {
pd := firewalldb.NewMockSessionDB()
pd.AddPair(sessionID, sessionID)
err := pd.AddPrivacyFlags(sessionID, session.PrivacyFlags{})
require.NoError(t, err)
msg := &lnrpc.ForwardingHistoryResponse{
ForwardingEvents: []*lnrpc.ForwardingEvent{