Skip to content

Commit 7024b80

Browse files
committed
fix: Refuse to use private keys that are group or world readable
- Added common.ValidatePrivateKeyFilePermissions. - LoadArmoredKey now rejects regular private-key files with any group/other permissions: mode.Perm() & 0077 != 0. - NewFileSigner now reuses common.LoadArmoredKey, removing duplicated key decoding. - LoadWormchainPrivKey now validates permissions before reading. - Added tests for unsafe 0644 key files in: - node/pkg/common/armoredKey_test.go - node/pkg/guardiansigner/guardiansigner_test.go - node/pkg/wormconn/wormchain_key_test.go - Updated query tests to copy the checked-in dev key into a temp 0600 file before loading. - Add unit test to maintain code coverage that would've dropped due to refactoring
1 parent dadcdf3 commit 7024b80

9 files changed

Lines changed: 162 additions & 59 deletions

File tree

.coverage-baseline

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ github.com/certusone/wormhole/node 0.0
66
github.com/certusone/wormhole/node/cmd 0.0
77
github.com/certusone/wormhole/node/cmd/ccq 15.5
88
github.com/certusone/wormhole/node/cmd/debug 0.0
9-
github.com/certusone/wormhole/node/cmd/guardiand 21.6
10-
github.com/certusone/wormhole/node/cmd/spy 31.1
9+
github.com/certusone/wormhole/node/cmd/guardiand 21.5
10+
github.com/certusone/wormhole/node/cmd/spy 31.2
1111
github.com/certusone/wormhole/node/cmd/txverifier 0.0
1212
github.com/certusone/wormhole/node/hack/accountant 0.0
1313
github.com/certusone/wormhole/node/hack/encrypt 0.0
@@ -23,21 +23,22 @@ github.com/certusone/wormhole/node/hack/repair_eth 0.0
2323
github.com/certusone/wormhole/node/hack/repair_solana 0.0
2424
github.com/certusone/wormhole/node/hack/solana 0.0
2525
github.com/certusone/wormhole/node/pkg/accountant 49.8
26-
github.com/certusone/wormhole/node/pkg/adminrpc 49.2
26+
github.com/certusone/wormhole/node/pkg/adminrpc 50.5
2727
github.com/certusone/wormhole/node/pkg/altpub 87.4
28-
github.com/certusone/wormhole/node/pkg/common 62.5
28+
github.com/certusone/wormhole/node/pkg/common 69.1
2929
github.com/certusone/wormhole/node/pkg/db 76.6
3030
github.com/certusone/wormhole/node/pkg/devnet 41.2
31-
github.com/certusone/wormhole/node/pkg/governor 49.7
32-
github.com/certusone/wormhole/node/pkg/guardiansigner 42.3
31+
github.com/certusone/wormhole/node/pkg/governor 49.6
32+
github.com/certusone/wormhole/node/pkg/guardiansigner 39.7
3333
github.com/certusone/wormhole/node/pkg/gwrelayer 31.9
34-
github.com/certusone/wormhole/node/pkg/manager 23.3
34+
github.com/certusone/wormhole/node/pkg/manager 29.3
3535
github.com/certusone/wormhole/node/pkg/manager/delegatedmanagersetabi 0.0
36-
github.com/certusone/wormhole/node/pkg/manager/dogecoin 93.8
37-
github.com/certusone/wormhole/node/pkg/node 65.2
36+
github.com/certusone/wormhole/node/pkg/manager/dogecoin 94.0
37+
github.com/certusone/wormhole/node/pkg/manager/xrpl 91.9
38+
github.com/certusone/wormhole/node/pkg/node 67.4
3839
github.com/certusone/wormhole/node/pkg/notary 36.9
3940
github.com/certusone/wormhole/node/pkg/p2p 40.3
40-
github.com/certusone/wormhole/node/pkg/processor 10.2
41+
github.com/certusone/wormhole/node/pkg/processor 25.0
4142
github.com/certusone/wormhole/node/pkg/proto/gossip/v1 0.0
4243
github.com/certusone/wormhole/node/pkg/proto/node/v1 0.0
4344
github.com/certusone/wormhole/node/pkg/proto/prometheus/v1 0.0
@@ -47,28 +48,31 @@ github.com/certusone/wormhole/node/pkg/publicrpc 12.4
4748
github.com/certusone/wormhole/node/pkg/query 70.2
4849
github.com/certusone/wormhole/node/pkg/random 0.0
4950
github.com/certusone/wormhole/node/pkg/readiness 0.0
51+
github.com/certusone/wormhole/node/pkg/suiclient 71.7
5052
github.com/certusone/wormhole/node/pkg/supervisor 0.0
5153
github.com/certusone/wormhole/node/pkg/telemetry 16.0
5254
github.com/certusone/wormhole/node/pkg/telemetry/prom_remote_write 59.5
5355
github.com/certusone/wormhole/node/pkg/txverifier 54.7
5456
github.com/certusone/wormhole/node/pkg/version 0.0
5557
github.com/certusone/wormhole/node/pkg/watchers/algorand 30.4
5658
github.com/certusone/wormhole/node/pkg/watchers/aptos 29.8
57-
github.com/certusone/wormhole/node/pkg/watchers/cosmwasm 0.0
58-
github.com/certusone/wormhole/node/pkg/watchers/evm 23.6
59-
github.com/certusone/wormhole/node/pkg/watchers/evm/connectors 43.6
59+
github.com/certusone/wormhole/node/pkg/watchers/cosmwasm 2.3
60+
github.com/certusone/wormhole/node/pkg/watchers/evm 23.5
61+
github.com/certusone/wormhole/node/pkg/watchers/evm/connectors 59.3
6062
github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/delegated_guardians 0.0
6163
github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi 0.0
6264
github.com/certusone/wormhole/node/pkg/watchers/evm/custom_consistency_level_abi 0.0
6365
github.com/certusone/wormhole/node/pkg/watchers/evm/finalizers/optimismctcabi 0.0
6466
github.com/certusone/wormhole/node/pkg/watchers/evm/verify_chain_config 0.0
6567
github.com/certusone/wormhole/node/pkg/watchers/ibc 22.0
6668
github.com/certusone/wormhole/node/pkg/watchers/mock 0.0
67-
github.com/certusone/wormhole/node/pkg/watchers/near 75.7
69+
github.com/certusone/wormhole/node/pkg/watchers/near 76.0
6870
github.com/certusone/wormhole/node/pkg/watchers/near/nearapi 61.8
6971
github.com/certusone/wormhole/node/pkg/watchers/near/nearapi/mock 0.0
70-
github.com/certusone/wormhole/node/pkg/watchers/solana 39.9
71-
github.com/certusone/wormhole/node/pkg/watchers/sui 7.9
72-
github.com/certusone/wormhole/node/pkg/wormconn 0.0
72+
github.com/certusone/wormhole/node/pkg/watchers/solana 41.0
73+
github.com/certusone/wormhole/node/pkg/watchers/sui 26.9
74+
github.com/certusone/wormhole/node/pkg/watchers/xrpl 72.8
75+
github.com/certusone/wormhole/node/pkg/watchers/xrpl/currencycodec 100.0
76+
github.com/certusone/wormhole/node/pkg/wormconn 2.3
7377
github.com/wormhole-foundation/wormhole/sdk 70.2
74-
github.com/wormhole-foundation/wormhole/sdk/vaa 61.4
78+
github.com/wormhole-foundation/wormhole/sdk/vaa 66.8

node/pkg/common/armoredKey.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ func LoadGuardianKey(filename string, unsafeDevMode bool) (*ecdsa.PrivateKey, er
2424

2525
// LoadArmoredKey loads a serialized key from disk.
2626
func LoadArmoredKey(filename string, blockType string, unsafeDevMode bool) (*ecdsa.PrivateKey, error) {
27+
if err := ValidatePrivateKeyFilePermissions(filename); err != nil {
28+
return nil, err
29+
}
30+
2731
f, err := os.Open(filename)
2832
if err != nil {
2933
return nil, fmt.Errorf("failed to open file: %w", err)
3034
}
35+
defer f.Close()
3136

3237
p, err := armor.Decode(f)
3338
if err != nil {
@@ -61,6 +66,20 @@ func LoadArmoredKey(filename string, blockType string, unsafeDevMode bool) (*ecd
6166
return gk, nil
6267
}
6368

69+
// ValidatePrivateKeyFilePermissions rejects regular private-key files that are readable by group or other users.
70+
func ValidatePrivateKeyFilePermissions(filename string) error {
71+
info, err := os.Stat(filename)
72+
if err != nil {
73+
return fmt.Errorf("failed to stat private key file: %w", err)
74+
}
75+
76+
if info.Mode().IsRegular() && info.Mode().Perm()&0077 != 0 {
77+
return fmt.Errorf("private key file %q has insecure permissions %o; expected no group or other permissions", filename, info.Mode().Perm())
78+
}
79+
80+
return nil
81+
}
82+
6483
// WriteArmoredKey serializes a key and writes it to disk.
6584
func WriteArmoredKey(key *ecdsa.PrivateKey, description string, filename string, blockType string, unsafe bool) error {
6685
if _, err := os.Stat(filename); !os.IsNotExist(err) {

node/pkg/common/armoredKey_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package common
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/crypto"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestLoadArmoredKeyRejectsUnsafePermissions(t *testing.T) {
13+
key, err := crypto.GenerateKey()
14+
require.NoError(t, err)
15+
keyPath := t.TempDir() + "/guardian.key"
16+
require.NoError(t, WriteArmoredKey(key, "test key", keyPath, GuardianKeyArmoredBlock, false))
17+
require.NoError(t, os.Chmod(keyPath, 0644))
18+
19+
loadedKey, err := LoadArmoredKey(keyPath, GuardianKeyArmoredBlock, false)
20+
21+
require.Error(t, err)
22+
assert.Nil(t, loadedKey)
23+
assert.Contains(t, err.Error(), "insecure permissions")
24+
}
25+
26+
func TestLoadArmoredKeyAllowsOwnerOnlyPermissions(t *testing.T) {
27+
key, err := crypto.GenerateKey()
28+
require.NoError(t, err)
29+
keyPath := t.TempDir() + "/guardian.key"
30+
require.NoError(t, WriteArmoredKey(key, "test key", keyPath, GuardianKeyArmoredBlock, false))
31+
32+
loadedKey, err := LoadArmoredKey(keyPath, GuardianKeyArmoredBlock, false)
33+
34+
require.NoError(t, err)
35+
assert.Equal(t, key, loadedKey)
36+
}

node/pkg/common/scissors.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ func StartRunnable(ctx context.Context, errC chan error, catchPanics bool, name
6565
default:
6666
err = fmt.Errorf("%s: %v", name, x)
6767
}
68+
ScissorsPanicsCaught.WithLabelValues(name).Inc()
6869
// We don't want this to hang if the listener has already gone away.
6970
select {
7071
case errC <- err:
7172
default:
7273
}
73-
ScissorsPanicsCaught.WithLabelValues(name).Inc()
7474

7575
}
7676
}()
@@ -83,11 +83,11 @@ func StartRunnable(ctx context.Context, errC chan error, catchPanics bool, name
8383
func startRunnable(ctx context.Context, errC chan error, name string, runnable supervisor.Runnable) {
8484
err := runnable(ctx)
8585
if err != nil {
86+
ScissorsErrorsCaught.WithLabelValues(name).Inc()
8687
// We don't want this to hang if the listener has already gone away.
8788
select {
8889
case errC <- err:
8990
default:
9091
}
91-
ScissorsErrorsCaught.WithLabelValues(name).Inc()
9292
}
9393
}

node/pkg/guardiansigner/filesigner.go

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@ package guardiansigner
33
import (
44
"context"
55
"crypto/ecdsa"
6-
"errors"
76
"fmt"
8-
"os"
97

108
"github.com/ethereum/go-ethereum/crypto"
119
ethcrypto "github.com/ethereum/go-ethereum/crypto"
12-
"google.golang.org/protobuf/proto"
1310

1411
"github.com/certusone/wormhole/node/pkg/common"
15-
nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1"
16-
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck // Package is deprecated but we need it in the codebase still.
1712
)
1813

1914
// FileSigner is a signer that loads a guardian key from a file. The URI is expected to be
@@ -35,38 +30,9 @@ func NewFileSigner(_ context.Context, unsafeDevMode bool, signerKeyPath string)
3530
keyPath: signerKeyPath,
3631
}
3732

38-
f, err := os.Open(signerKeyPath)
33+
gk, err := common.LoadArmoredKey(signerKeyPath, GuardianKeyArmoredBlock, unsafeDevMode)
3934
if err != nil {
40-
return nil, fmt.Errorf("failed to open file: %w", err)
41-
}
42-
43-
p, err := armor.Decode(f)
44-
if err != nil {
45-
return nil, fmt.Errorf("failed to read armored file: %w", err)
46-
}
47-
48-
if p.Type != GuardianKeyArmoredBlock {
49-
return nil, fmt.Errorf("invalid block type: %s", p.Type)
50-
}
51-
52-
b, err := common.SafeRead(p.Body)
53-
if err != nil {
54-
return nil, fmt.Errorf("failed to read file: %w", err)
55-
}
56-
57-
var m nodev1.GuardianKey
58-
err = proto.Unmarshal(b, &m)
59-
if err != nil {
60-
return nil, fmt.Errorf("failed to deserialize protobuf: %w", err)
61-
}
62-
63-
if !unsafeDevMode && m.UnsafeDeterministicKey {
64-
return nil, errors.New("refusing to use deterministic key in production")
65-
}
66-
67-
gk, err := ethcrypto.ToECDSA(m.Data)
68-
if err != nil {
69-
return nil, fmt.Errorf("failed to deserialize raw key data: %w", err)
35+
return nil, err
7036
}
7137

7238
fileSigner.privateKey = gk

node/pkg/guardiansigner/guardiansigner_test.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package guardiansigner
33
import (
44
"context"
55
"encoding/hex"
6+
"os"
67
"testing"
78

9+
"github.com/certusone/wormhole/node/pkg/common"
810
"github.com/ethereum/go-ethereum/crypto"
911
ethcrypto "github.com/ethereum/go-ethereum/crypto"
1012
"github.com/stretchr/testify/assert"
@@ -61,8 +63,12 @@ func TestFileSignerNonExistentFile(t *testing.T) {
6163

6264
func TestFileSigner(t *testing.T) {
6365
ctx := context.Background()
64-
fileUri := "file://../query/dev.guardian.key"
65-
expectedEthAddress := "0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"
66+
key, err := crypto.GenerateKey()
67+
require.NoError(t, err)
68+
keyPath := t.TempDir() + "/guardian.key"
69+
require.NoError(t, common.WriteArmoredKey(key, "test key", keyPath, GuardianKeyArmoredBlock, false))
70+
fileUri := "file://" + keyPath
71+
expectedEthAddress := ethcrypto.PubkeyToAddress(key.PublicKey).Hex()
6672

6773
// For each file signer generation attempt, check:
6874
// That the signer returned is not nil
@@ -102,6 +108,43 @@ func TestFileSigner(t *testing.T) {
102108

103109
}
104110

111+
func TestFileSignerRejectsUnsafePermissions(t *testing.T) {
112+
key, err := crypto.GenerateKey()
113+
require.NoError(t, err)
114+
keyPath := t.TempDir() + "/guardian.key"
115+
require.NoError(t, common.WriteArmoredKey(key, "test key", keyPath, GuardianKeyArmoredBlock, false))
116+
require.NoError(t, os.Chmod(keyPath, 0644))
117+
118+
fileSigner, err := NewFileSigner(context.Background(), true, keyPath)
119+
120+
require.Error(t, err)
121+
assert.Nil(t, fileSigner)
122+
assert.Contains(t, err.Error(), "insecure permissions")
123+
}
124+
125+
func TestGeneratedSigner(t *testing.T) {
126+
ctx := context.Background()
127+
key, err := crypto.GenerateKey()
128+
require.NoError(t, err)
129+
130+
signer, err := GenerateSignerWithPrivatekeyUnsafe(key)
131+
require.NoError(t, err)
132+
assert.Equal(t, "generated", signer.TypeAsString())
133+
assert.Equal(t, key.PublicKey, signer.PublicKey(ctx))
134+
135+
hash := crypto.Keccak256Hash([]byte("data"))
136+
sig, err := signer.Sign(ctx, hash.Bytes())
137+
require.NoError(t, err)
138+
139+
valid, err := signer.Verify(ctx, sig, hash.Bytes())
140+
require.NoError(t, err)
141+
assert.True(t, valid)
142+
143+
valid, err = signer.Verify(ctx, sig, crypto.Keccak256Hash([]byte("other data")).Bytes())
144+
require.NoError(t, err)
145+
assert.False(t, valid)
146+
}
147+
105148
func TestAmazonKmsAdjustBufferSize(t *testing.T) {
106149

107150
bytes_30_null_0102, _ := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000102")

node/pkg/query/query_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/hex"
88
"fmt"
99
"math"
10+
"os"
1011
"strconv"
1112
"strings"
1213
"sync"
@@ -417,7 +418,16 @@ func createQueryHandlerForTestWithoutPublisher(t *testing.T, ctx context.Context
417418
md := mockData{}
418419
var err error
419420

420-
md.sk, err = common.LoadGuardianKey("dev.guardian.key", true)
421+
devKey, err := os.ReadFile("dev.guardian.key")
422+
require.NoError(t, err)
423+
devKeyFile, err := os.CreateTemp(t.TempDir(), "dev.guardian.key")
424+
require.NoError(t, err)
425+
_, err = devKeyFile.Write(devKey)
426+
require.NoError(t, err)
427+
require.NoError(t, devKeyFile.Close())
428+
devKeyPath := devKeyFile.Name()
429+
430+
md.sk, err = common.LoadGuardianKey(devKeyPath, true)
421431
require.NoError(t, err)
422432
require.NotNil(t, md.sk)
423433

node/pkg/wormconn/wormchain_key.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ package wormconn
33
import (
44
"os"
55

6+
"github.com/certusone/wormhole/node/pkg/common"
67
"github.com/cosmos/cosmos-sdk/crypto"
78
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
89
)
910

1011
func LoadWormchainPrivKey(path string, passPhrase string) (cryptotypes.PrivKey, error) {
12+
if err := common.ValidatePrivateKeyFilePermissions(path); err != nil {
13+
return nil, err
14+
}
15+
1116
armor, err := os.ReadFile(path)
1217
if err != nil {
1318
return nil, err
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package wormconn
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestLoadWormchainPrivKeyRejectsUnsafePermissions(t *testing.T) {
12+
keyPath := t.TempDir() + "/wormchain.key"
13+
require.NoError(t, os.WriteFile(keyPath, []byte("not a valid encrypted key"), 0644)) // #nosec G306 -- This test verifies unsafe key-file permissions are rejected.
14+
15+
key, err := LoadWormchainPrivKey(keyPath, "passphrase")
16+
17+
require.Error(t, err)
18+
assert.Nil(t, key)
19+
assert.Contains(t, err.Error(), "insecure permissions")
20+
}

0 commit comments

Comments
 (0)