rpcserver+firewall: obfuscate configuration

We obfuscate pubkeys, channel points and ids entered in configurations.

The channel id lengths for different block heights can be checked with:
```python
len(str(1 << 40 | 2923 << 16 | 30))
len(str(10_000_000 << 40 | 2923 << 16 | 30))
```
This commit is contained in:
bitromortac 2023-03-01 17:19:21 +01:00
parent 5580d6861d
commit eb58941e76
No known key found for this signature in database
GPG key ID: 1965063FC13BEBE2
3 changed files with 378 additions and 1 deletions

View file

@ -3,9 +3,13 @@ package firewall
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"github.com/lightninglabs/lightning-terminal/firewalldb"
@ -30,6 +34,15 @@ const (
// between which timeVariation can be set.
minTimeVariation = time.Minute
maxTimeVariation = time.Duration(24) * time.Hour
// min and maxChanIDLen are the lengths to consider an int to be a
// channel id. 13 corresponds to block height 1 and 20 to block height
// 10_000_000.
minChanIDLen = 13
maxChanIDLen = 20
// pubKeyLen is the length of a node pubkey.
pubKeyLen = 66
)
var (
@ -845,3 +858,145 @@ func CryptoRandIntn(n int) (int, error) {
return int(nBig.Int64()), nil
}
// ObfuscateConfig alters the config string by replacing sensitive data with
// random values and returns new replacement pairs. We only substitute items in
// strings, numbers are left unchanged.
func ObfuscateConfig(db firewalldb.PrivacyMapReader, configB []byte) ([]byte,
map[string]string, error) {
if len(configB) == 0 {
return nil, nil, nil
}
// We assume that the config is a json dict.
var configMap map[string]any
err := json.Unmarshal(configB, &configMap)
if err != nil {
return nil, nil, err
}
privMapPairs := make(map[string]string)
newConfigMap := make(map[string]any)
for k, v := range configMap {
// We only substitute items in lists.
list, ok := v.([]any)
if !ok {
newConfigMap[k] = v
continue
}
// We only substitute items in lists of strings.
stringList := make([]string, len(list))
anyString := false
allStrings := true
for i, item := range list {
item, ok := item.(string)
allStrings = allStrings && ok
anyString = anyString || ok
if !ok {
continue
}
stringList[i] = item
}
if anyString && !allStrings {
return nil, nil, fmt.Errorf("invalid config, "+
"expected list of only strings for key %s", k)
} else if !anyString {
newConfigMap[k] = v
continue
}
obfuscatedValues := make([]string, len(stringList))
for i, value := range stringList {
value := strings.TrimSpace(value)
// We first check if we have a mapping for this value
// already.
obfVal, haveValue := db.GetPseudo(value)
if haveValue {
obfuscatedValues[i] = obfVal
continue
}
// We check if we have obfuscated this value already in
// this run.
obfVal, haveValue = privMapPairs[value]
if haveValue {
obfuscatedValues[i] = obfVal
continue
}
// From here on we create new obfuscated values.
// Try to replace with a chan point.
_, _, err := firewalldb.DecodeChannelPoint(value)
if err == nil {
obfVal, err = firewalldb.NewPseudoChanPoint()
if err != nil {
return nil, nil, err
}
obfuscatedValues[i] = obfVal
privMapPairs[value] = obfVal
continue
}
// If the value is a pubkey, replace it with a random
// value.
_, err = hex.DecodeString(value)
if err == nil && len(value) == pubKeyLen {
obfVal, err := firewalldb.NewPseudoStr(
len(value),
)
if err != nil {
return nil, nil, err
}
obfuscatedValues[i] = obfVal
privMapPairs[value] = obfVal
continue
}
// If the value is a channel id, replace it with
// a random value.
_, err = strconv.ParseInt(value, 10, 64)
length := len(value)
// Channel ids can have different lenghts depending on
// the blockheight, 20 is equivalent to 10E9 blocks.
if err == nil && minChanIDLen <= length &&
length <= maxChanIDLen {
obfVal, err := firewalldb.NewPseudoStr(length)
if err != nil {
return nil, nil, err
}
obfuscatedValues[i] = obfVal
privMapPairs[value] = obfVal
continue
}
// If we don't have a replacement for this value, we
// just leave it as is.
obfuscatedValues[i] = value
}
newConfigMap[k] = obfuscatedValues
}
// Marshal the map back into a JSON blob.
newConfigB, err := json.Marshal(newConfigMap)
if err != nil {
return nil, nil, err
}
return newConfigB, privMapPairs, nil
}

View file

@ -2,6 +2,7 @@ package firewall
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
@ -718,6 +719,196 @@ func TestHideBool(t *testing.T) {
require.False(t, val)
}
// TestObfuscateConfig tests that we substitute substrings in the config
// correctly.
func TestObfuscateConfig(t *testing.T) {
tests := []struct {
name string
config []byte
knownMap map[string]string
expectedNewPairs int
expectErr bool
notExpectSameLen bool
}{
{
name: "empty",
},
{
// We substitue pubkeys of different forms.
name: "several pubkeys",
config: []byte(`{"version":1,"list":` +
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
expectedNewPairs: 4,
},
{
// We don't generate new pairs for pubkeys that we
// already have a mapping.
name: "several pubkeys with known replacement or duplicates",
config: []byte(`{"version":1,"list":` +
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
knownMap: map[string]string{
"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4": "123456789012345678901234567890123456789012345678901234567890123456",
},
expectedNewPairs: 3,
},
{
// We don't substitute unknown items.
name: "all invalid pubkeys",
config: []byte(`{"version":1,"list":` +
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11",` +
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4dead",` +
`"x86b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
expectedNewPairs: 0,
},
{
// We only substitute channel ids that have a sane
// format.
name: "channel ids",
config: []byte(`{"version":1,"list":` +
`["1",` +
`"12345",` +
`"1234567890123",` +
`"1234567890123456789",` +
`"123456789012345678901"]}`),
expectedNewPairs: 2,
},
{
// We obfuscate channel points, the character length may
// vary due to the output index.
name: "channel points",
config: []byte(`{"version":1,"list":` +
`["0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
`"e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca3:1",` +
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:3000"]}`),
expectedNewPairs: 2,
notExpectSameLen: true,
},
{
// We only act on items that are in lists of strings.
name: "single pubkey with another field",
config: []byte(`{"version":1,"list":` +
`["586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"],` +
`"another":"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85"}`),
expectedNewPairs: 1,
},
{
// We don't obfuscate any numbers even though they may
// be channel ids. This is to be able to set numerical
// values in the range of channel ids.
name: "number",
config: []byte(`{"version":1,"number":12345678901234567890}`),
expectedNewPairs: 0,
},
{
// We don't allow to mix strings with other types, which
// may be a configuration mistake.
name: "list of invalid types",
config: []byte(`{"version":1,"channels":` +
`[12345,` +
`"e092708c9e737115ff14a85ab65466561280d77c1b8cd666bc655536ad81ccca:1"]}`),
expectErr: true,
expectedNewPairs: 0,
},
{
// A list of numbers is not obfuscated. Those can be
// useful to submit histograms for example.
name: "channel ids",
config: []byte(`{"version":1,"list":` +
`[1,` +
`12345,` +
`1234567890123,` +
`1234567890123456789,` +
`123456789012345678901]}`),
expectedNewPairs: 0,
},
}
// assertConfigStructure checks that the structure of the config is
// preserved.
assertConfigStructure := func(wantConfig, gotConfig []byte) {
t.Helper()
if len(wantConfig) == 0 {
require.Equal(t, wantConfig, gotConfig)
return
}
var wantConfigMap map[string]any
err := json.Unmarshal(wantConfig, &wantConfigMap)
require.NoError(t, err)
var gotConfigMap map[string]any
err = json.Unmarshal(gotConfig, &gotConfigMap)
require.NoError(t, err)
// We test that the number of top level items is the same.
require.Equal(t, len(wantConfigMap), len(gotConfigMap))
listLen := func(config map[string]any) int {
for k, v := range config {
if k == "list" {
list, ok := v.([]interface{})
require.True(t, ok)
return len(list)
}
}
return 0
}
// We test that we have the same number of items in the list.
require.Equal(t, listLen(wantConfigMap), listLen(gotConfigMap))
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db := firewalldb.NewPrivacyMapPairs(tt.knownMap)
config, privMapPairs, err := ObfuscateConfig(
db, tt.config,
)
if tt.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// We expect the config to be obfuscated in any parts
// only if there is sensitive data.
if tt.expectedNewPairs > 0 {
require.NotEqual(t, config, tt.config)
}
// We check that we recognized the correct number of new
// substitutions.
require.Equal(t, tt.expectedNewPairs,
len(privMapPairs))
// We expect the same number of items in the config
// after obfuscation.
assertConfigStructure(tt.config, config)
// We don't perform exact length checks for cases where
// we know the length can change.
if !tt.notExpectSameLen {
require.Equal(t, len(tt.config), len(config))
}
})
}
}
// mean computes the mean of the given slice of numbers.
func mean(numbers []uint64) uint64 {
sum := uint64(0)

View file

@ -1125,6 +1125,37 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
prevSessionPub = linkedGroupSession.LocalPublicKey
}
// The feature configurations may contain sensitive data like pubkeys,
// which we replace here and add to the privacy map.
obfuscatedConfig := make(session.FeaturesConfig, len(clientConfig))
if privacy {
for name, configB := range clientConfig {
configB, privMapPairs, err := firewall.ObfuscateConfig(
knownPrivMapPairs, configB,
)
if err != nil {
return nil, err
}
// Store the new privacy map pairs in the newPrivMap
// pairs map so that they are later persisted to the
// real priv map db.
for k, v := range privMapPairs {
newPrivMapPairs[k] = v
}
// Also add the new pairs to the known set of pairs.
err = knownPrivMapPairs.Add(privMapPairs)
if err != nil {
return nil, err
}
obfuscatedConfig[name] = configB
}
} else {
obfuscatedConfig = clientConfig
}
// Register all the privacy map pairs for this session ID.
privDB := s.cfg.privMap(sess.GroupID)
err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error {
@ -1143,7 +1174,7 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
// Attempt to register the session with the Autopilot server.
remoteKey, err := s.cfg.autopilot.RegisterSession(
ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer,
clientConfig, prevSessionPub, linkSig,
obfuscatedConfig, prevSessionPub, linkSig,
)
if err != nil {
return nil, fmt.Errorf("error registering session with "+