From 08775c5ab00e94cf0e3d37bc7dd2574295e36c67 Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Wed, 29 Jan 2020 12:41:02 +0000 Subject: [PATCH] go/registry: Handle the old and busted node descriptor envelope > Crawling in my skin > These wounds, they will not heal --- .changelog/2614.feature.md | 23 ++ .../tendermint/apps/registry/genesis.go | 10 + .../apps/supplementarysanity/checks.go | 26 ++- go/oasis-node/cmd/debug/debug.go | 2 + .../cmd/debug/fixgenesis/fixgenesis.go | 219 ++++++++++++++++++ go/registry/api/api.go | 91 +++++--- go/registry/api/sanity_check.go | 19 +- 7 files changed, 348 insertions(+), 42 deletions(-) create mode 100644 .changelog/2614.feature.md create mode 100644 go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go diff --git a/.changelog/2614.feature.md b/.changelog/2614.feature.md new file mode 100644 index 00000000000..931dee1772c --- /dev/null +++ b/.changelog/2614.feature.md @@ -0,0 +1,23 @@ +go/registry: Handle the old and busted node descriptor envelope + +The old node descriptor envelope has one signature. The new envelope has +multiple signatures, to ensure that the node has access to the private +component of all public keys listed in the descriptor. + +The correct thing to do, from a security standpoint is to use a new set +of genesis node descriptors. Instead, this change facilitates the +transition in what is probably the worst possible way by: + + * Disabling signature verification entirely for node descriptors listed + in the genesis document (Technically this can be avoided, but there + are other changes to the node descriptor that require no verification + to be done if backward compatibility is desired). + + * Providing a conversion tool that fixes up the envelopes to the new + format. + + * Omitting descriptors that are obviously converted from state dumps. + +Note: Node descriptors that are using the now deprecated option to use +the entity key for signing are not supported at all, and backward +compatibility will NOT be maintained. diff --git a/go/consensus/tendermint/apps/registry/genesis.go b/go/consensus/tendermint/apps/registry/genesis.go index ab94cf7b5c1..8832d8caf73 100644 --- a/go/consensus/tendermint/apps/registry/genesis.go +++ b/go/consensus/tendermint/apps/registry/genesis.go @@ -151,6 +151,9 @@ func (rq *registryQuerier) Genesis(ctx context.Context) (*registry.Genesis, erro } // We only want to keep the nodes that are validators. + // + // BUG: If the debonding period will apply to other nodes, + // then we need to basically persist everything. validatorNodes := make([]*node.MultiSignedNode, 0) for _, sn := range signedNodes { var n node.Node @@ -161,6 +164,13 @@ func (rq *registryQuerier) Genesis(ctx context.Context) (*registry.Genesis, erro if n.HasRoles(node.RoleValidator) { validatorNodes = append(validatorNodes, sn) } + + // We want to discard nodes that haven't bothered to re-register + // with the new multi-signed descriptor format. + if len(sn.MultiSigned.Signatures) < 2 { + // Too bad we can't log here. Oh well. + continue + } } nodeStatuses, err := rq.state.NodeStatuses() diff --git a/go/consensus/tendermint/apps/supplementarysanity/checks.go b/go/consensus/tendermint/apps/supplementarysanity/checks.go index 3900c8dafd3..e4fa990105e 100644 --- a/go/consensus/tendermint/apps/supplementarysanity/checks.go +++ b/go/consensus/tendermint/apps/supplementarysanity/checks.go @@ -59,15 +59,23 @@ func checkRegistry(state *iavl.MutableTree, now epochtime.EpochTime) error { return fmt.Errorf("SanityCheckRuntimes: %w", err) } - // Check nodes. - nodes, err := st.SignedNodes() - if err != nil { - return fmt.Errorf("SignedNodes: %w", err) - } - err = registry.SanityCheckNodes(logger, params, nodes, seenEntities, runtimeLookup, false, now) - if err != nil { - return fmt.Errorf("SanityCheckNodes: %w", err) - } + // We can't run this check till everyone transitions to the new + // descriptor format, since there's currently no way to distinguish + // between nodes registered at genesis (old signature format) and + // nodes registered at runtime (MUST be new signature format). + + /* + // Check nodes. + nodes, err := st.SignedNodes() + if err != nil { + return fmt.Errorf("SignedNodes: %w", err) + } + err = registry.SanityCheckNodes(logger, params, nodes, seenEntities, runtimeLookup, false, now) + if err != nil { + return fmt.Errorf("SanityCheckNodes: %w", err) + } + */ + _, _ = seenEntities, runtimeLookup return nil } diff --git a/go/oasis-node/cmd/debug/debug.go b/go/oasis-node/cmd/debug/debug.go index eae5dcdd5d2..66bfaaab7e0 100644 --- a/go/oasis-node/cmd/debug/debug.go +++ b/go/oasis-node/cmd/debug/debug.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/byzantine" + "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/fixgenesis" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/storage" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/tendermint" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/txsource" @@ -21,6 +22,7 @@ func Register(parentCmd *cobra.Command) { tendermint.Register(debugCmd) byzantine.Register(debugCmd) txsource.Register(debugCmd) + fixgenesis.Register(debugCmd) parentCmd.AddCommand(debugCmd) } diff --git a/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go b/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go new file mode 100644 index 00000000000..22c3018277e --- /dev/null +++ b/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go @@ -0,0 +1,219 @@ +// Package fixgenesis implements the fix-genesis command. +package fixgenesis + +import ( + "encoding/json" + "io/ioutil" + "os" + "time" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + beacon "github.com/oasislabs/oasis-core/go/beacon/api" + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + "github.com/oasislabs/oasis-core/go/common/entity" + "github.com/oasislabs/oasis-core/go/common/logging" + "github.com/oasislabs/oasis-core/go/common/node" + consensus "github.com/oasislabs/oasis-core/go/consensus/genesis" + epochtime "github.com/oasislabs/oasis-core/go/epochtime/api" + genesis "github.com/oasislabs/oasis-core/go/genesis/api" + keymanager "github.com/oasislabs/oasis-core/go/keymanager/api" + cmdCommon "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common" + "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/flags" + registry "github.com/oasislabs/oasis-core/go/registry/api" + roothash "github.com/oasislabs/oasis-core/go/roothash/api" + scheduler "github.com/oasislabs/oasis-core/go/scheduler/api" + staking "github.com/oasislabs/oasis-core/go/staking/api" +) + +const cfgNewGenesis = "genesis.new_file" + +var ( + fixGenesisCmd = &cobra.Command{ + Use: "fix-genesis", + Short: "fix a genesis document", + Run: doFixGenesis, + } + + newGenesisFlag = flag.NewFlagSet("", flag.ContinueOnError) + + logger = logging.GetLogger("cmd/debug/fix-genesis") +) + +type oldDocument struct { + // Height is the block height at which the document was generated. + Height int64 `json:"height"` + // Time is the time the genesis block was constructed. + Time time.Time `json:"genesis_time"` + // ChainID is the ID of the chain. + ChainID string `json:"chain_id"` + // EpochTime is the timekeeping genesis state. + EpochTime epochtime.Genesis `json:"epochtime"` + // Registry is the registry genesis state. + Registry oldRegistry `json:"registry"` + // RootHash is the roothash genesis state. + RootHash roothash.Genesis `json:"roothash"` + // Staking is the staking genesis state. + Staking staking.Genesis `json:"staking"` + // KeyManager is the key manager genesis state. + KeyManager keymanager.Genesis `json:"keymanager"` + // Scheduler is the scheduler genesis state. + Scheduler scheduler.Genesis `json:"scheduler"` + // Beacon is the beacon genesis state. + Beacon beacon.Genesis `json:"beacon"` + // Consensus is the consensus genesis state. + Consensus consensus.Genesis `json:"consensus"` + // HaltEpoch is the epoch height at which the network will stop processing + // any transactions and will halt. + HaltEpoch epochtime.EpochTime `json:"halt_epoch"` + // Extra data is arbitrary extra data that is part of the + // genesis block but is otherwise ignored by the protocol. + ExtraData map[string][]byte `json:"extra_data"` +} + +type oldRegistry struct { + // Parameters are the registry consensus parameters. + Parameters registry.ConsensusParameters `json:"params"` + // Entities is the initial list of entities. + Entities []*entity.SignedEntity `json:"entities,omitempty"` + // Runtimes is the initial list of runtimes. + Runtimes []*registry.SignedRuntime `json:"runtimes,omitempty"` + // SuspendedRuntimes is the list of suspended runtimes. + SuspendedRuntimes []*registry.SignedRuntime `json:"suspended_runtimes,omitempty"` + // Nodes is the initial list of nodes. + Nodes []*oldSignedNode `json:"nodes,omitempty"` + // NodeStatuses is a set of node statuses. + NodeStatuses map[signature.PublicKey]*registry.NodeStatus `json:"node_statuses,omitempty"` +} + +type oldSignedNode struct { + signature.Signed +} + +func doFixGenesis(cmd *cobra.Command, args []string) { + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + + // Load the old genesis document. + f := flags.GenesisFile() + raw, err := ioutil.ReadFile(f) + if err != nil { + logger.Error("failed to open genesis file", + "err", err, + ) + os.Exit(1) + } + + // Parse as the old format. At some point all the important things + // will be versioned, but this is not that time. + var oldDoc oldDocument + if err = json.Unmarshal(raw, &oldDoc); err != nil { + logger.Error("failed to parse old genesis file", + "err", err, + ) + os.Exit(1) + } + + // Actually fix the genesis document. + newDoc, err := updateGenesisDoc(&oldDoc) + if err != nil { + logger.Error("failed to fix genesis document", + "err", err, + ) + os.Exit(1) + } + + // Validate the new genesis document. + if err = newDoc.SanityCheck(); err != nil { + logger.Error("new genesis document sanity check failed", + "err", err, + ) + os.Exit(1) + } + + // Write out the new genesis document. + w, shouldClose, err := cmdCommon.GetOutputWriter(cmd, cfgNewGenesis) + if err != nil { + logger.Error("failed to get writer for fixed genesis file", + "err", err, + ) + os.Exit(1) + } + if shouldClose { + defer w.Close() + } + if raw, err = json.Marshal(newDoc); err != nil { + logger.Error("failed to marshal fixed genesis document into JSON", + "err", err, + ) + os.Exit(1) + } + if _, err = w.Write(raw); err != nil { + logger.Error("failed to write new genesis file", + "err", err, + ) + os.Exit(1) + } +} + +func updateGenesisDoc(oldDoc *oldDocument) (*genesis.Document, error) { + // Create the new genesis document template. + newDoc := &genesis.Document{ + Height: oldDoc.Height, + Time: oldDoc.Time, + ChainID: oldDoc.ChainID, + EpochTime: oldDoc.EpochTime, + RootHash: oldDoc.RootHash, + Staking: oldDoc.Staking, + KeyManager: oldDoc.KeyManager, + Scheduler: oldDoc.Scheduler, + Beacon: oldDoc.Beacon, + Consensus: oldDoc.Consensus, + HaltEpoch: oldDoc.HaltEpoch, + ExtraData: oldDoc.ExtraData, + } + + // This currently is entirely registry genesis state changes. + oldReg, newReg := oldDoc.Registry, newDoc.Registry + + // First copy the registry things that have not changed. + newReg.Parameters = oldReg.Parameters + newReg.Entities = oldReg.Entities + newReg.Runtimes = oldReg.Runtimes + newReg.SuspendedRuntimes = oldReg.SuspendedRuntimes + newReg.NodeStatuses = oldReg.NodeStatuses + + // The node descriptor signature envelope format in the registry has + // changed. Convert to the new envelope. + // + // Note: Actually using the genesis document requires that some + // signature checks be disabled. + for _, osn := range oldReg.Nodes { + var nsn node.MultiSignedNode + nsn.MultiSigned.Signatures = []signature.Signature{osn.Signed.Signature} + + // TODO: Someone that understands the issue can also fix the node + // role flags here. + + nsn.MultiSigned.Blob = osn.Signed.Blob + + newReg.Nodes = append(newReg.Nodes, &nsn) + } + + return newDoc, nil +} + +// Register registers the fix-genesis sub-command and all of it's children. +func Register(parentCmd *cobra.Command) { + fixGenesisCmd.PersistentFlags().AddFlagSet(flags.GenesisFileFlags) + fixGenesisCmd.PersistentFlags().AddFlagSet(newGenesisFlag) + parentCmd.AddCommand(fixGenesisCmd) +} + +func init() { + newGenesisFlag.String(cfgNewGenesis, "genesis_fixed.json", "path to fixed genesis document") + _ = viper.BindPFlags(newGenesisFlag) +} diff --git a/go/registry/api/api.go b/go/registry/api/api.go index 5e03851c61a..6ad735dd281 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -385,11 +385,32 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo ctx = RegisterNodeSignatureContext } - if err := sigNode.Open(ctx, &n); err != nil { - logger.Error("RegisterNode: invalid signature", - "signed_node", sigNode, + // Sigh. Instead of having people regenerate some files for our test + // environment, instead we're allowing a transition by disabling some + // signature validation. + // + // Note: The entity signed case will fail to import, however as that + // functionality is gated behind DontBlameOasis, tough shit. + isOldNode := isOldNode(sigNode, isGenesis) + + if !isOldNode { + if err := sigNode.Open(ctx, &n); err != nil { + logger.Error("RegisterNode: invalid signature", + "signed_node", sigNode, + ) + return nil, nil, ErrInvalidSignature + } + } else { + if err := cbor.Unmarshal(sigNode.Blob, &n); err != nil { + logger.Error("RegisterNode: failed to unmarshal old-format descriptor", + "err", err, + "signed_node", sigNode, + ) + return nil, nil, fmt.Errorf("%w: failed to unmarshal old-format descriptor", ErrInvalidArgument) + } + logger.Warn("RegisterNode: bypassing old genesis descriptor signature verification", + "node", n, ) - return nil, nil, ErrInvalidSignature } // This should never happen, unless there's a bug in the caller. @@ -531,14 +552,16 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo ) return nil, nil, fmt.Errorf("%w: invalid consensus ID", ErrInvalidArgument) } - if !sigNode.MultiSigned.IsSignedBy(n.Consensus.ID) { - logger.Error("RegisterNode: not signed by consensus ID", - "signed_node", sigNode, - "node", n, - ) - return nil, nil, fmt.Errorf("%w: registration not signed by consensus ID", ErrInvalidArgument) + if !isOldNode { + if !sigNode.MultiSigned.IsSignedBy(n.Consensus.ID) { + logger.Error("RegisterNode: not signed by consensus ID", + "signed_node", sigNode, + "node", n, + ) + return nil, nil, fmt.Errorf("%w: registration not signed by consensus ID", ErrInvalidArgument) + } + expectedSigners = append(expectedSigners, n.Consensus.ID) } - expectedSigners = append(expectedSigners, n.Consensus.ID) consensusAddressRequired := n.HasRoles(ConsensusAddressRequiredRoles) if err := verifyAddresses(params, consensusAddressRequired, n.Consensus.Addresses); err != nil { addrs, _ := json.Marshal(n.Consensus.Addresses) @@ -582,14 +605,16 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo if err != nil { return nil, nil, err } - if !sigNode.MultiSigned.IsSignedBy(certPub) { - logger.Error("RegisterNode: not signed by TLS certificate key", - "signed_node", sigNode, - "node", n, - ) - return nil, nil, fmt.Errorf("%w: registration not signed by TLS certificate key", ErrInvalidArgument) + if !isOldNode { + if !sigNode.MultiSigned.IsSignedBy(certPub) { + logger.Error("RegisterNode: not signed by TLS certificate key", + "signed_node", sigNode, + "node", n, + ) + return nil, nil, fmt.Errorf("%w: registration not signed by TLS certificate key", ErrInvalidArgument) + } + expectedSigners = append(expectedSigners, certPub) } - expectedSigners = append(expectedSigners, certPub) // Validate P2PInfo. if !n.P2P.ID.IsValid() { @@ -598,14 +623,16 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo ) return nil, nil, fmt.Errorf("%w: invalid P2P ID", ErrInvalidArgument) } - if !sigNode.MultiSigned.IsSignedBy(n.P2P.ID) { - logger.Error("RegisterNode: not signed by P2P ID", - "signed_node", sigNode, - "node", n, - ) - return nil, nil, fmt.Errorf("%w: registration not signed by P2P ID", ErrInvalidArgument) + if !isOldNode { + if !sigNode.MultiSigned.IsSignedBy(n.P2P.ID) { + logger.Error("RegisterNode: not signed by P2P ID", + "signed_node", sigNode, + "node", n, + ) + return nil, nil, fmt.Errorf("%w: registration not signed by P2P ID", ErrInvalidArgument) + } + expectedSigners = append(expectedSigners, n.P2P.ID) } - expectedSigners = append(expectedSigners, n.P2P.ID) p2pAddressRequired := n.HasRoles(P2PAddressRequiredRoles) if err = verifyAddresses(params, p2pAddressRequired, n.P2P.Addresses); err != nil { addrs, _ := json.Marshal(n.P2P.Addresses) @@ -677,12 +704,14 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo } // Ensure that only the expected signatures are present, and nothing more. - if !sigNode.MultiSigned.IsOnlySignedBy(expectedSigners) { - logger.Error("RegisterNode: unexpected number of signatures", - "signed_node", sigNode, - "node", n, - ) - return nil, nil, fmt.Errorf("%w: unexpected number of signatures", ErrInvalidArgument) + if !isOldNode { + if !sigNode.MultiSigned.IsOnlySignedBy(expectedSigners) { + logger.Error("RegisterNode: unexpected number of signatures", + "signed_node", sigNode, + "node", n, + ) + return nil, nil, fmt.Errorf("%w: unexpected number of signatures", ErrInvalidArgument) + } } return &n, runtimes, nil diff --git a/go/registry/api/sanity_check.go b/go/registry/api/sanity_check.go index e44881a06e9..298156908b8 100644 --- a/go/registry/api/sanity_check.go +++ b/go/registry/api/sanity_check.go @@ -5,6 +5,7 @@ import ( "time" "github.com/oasislabs/oasis-core/go/common" + "github.com/oasislabs/oasis-core/go/common/cbor" "github.com/oasislabs/oasis-core/go/common/crypto/hash" "github.com/oasislabs/oasis-core/go/common/crypto/signature" "github.com/oasislabs/oasis-core/go/common/entity" @@ -14,6 +15,12 @@ import ( "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/flags" ) +func isOldNode(sigNode *node.MultiSignedNode, isGenesis bool) bool { + // Technically this will always have 1 signature after conversion, + // but every well-formed node will have more than 2 signatures. + return isGenesis && len(sigNode.MultiSigned.Signatures) < 2 +} + // SanityCheck does basic sanity checking on the genesis state. func (g *Genesis) SanityCheck(baseEpoch epochtime.EpochTime) error { logger := logging.GetLogger("genesis/sanity-check") @@ -125,10 +132,18 @@ func SanityCheckNodes( } for _, sn := range nodes { + isOldNode := isOldNode(sn, isGenesis) + // Open the node to get the referenced entity. var n node.Node - if err := sn.Open(RegisterGenesisNodeSignatureContext, &n); err != nil { - return fmt.Errorf("registry: sanity check failed: unable to open signed node") + if !isOldNode { + if err := sn.Open(RegisterGenesisNodeSignatureContext, &n); err != nil { + return fmt.Errorf("registry: sanity check failed: unable to open signed node") + } + } else { + if err := cbor.Unmarshal(sn.Blob, &n); err != nil { + return fmt.Errorf("registry: sanity check failed: unable to unmarshal old-format descriptor") + } } if !n.ID.IsValid() { return fmt.Errorf("registry: sanity check failed: node ID %s is invalid", n.ID.String())