Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Keyring #2557

Merged
merged 24 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/start-binary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:

- name: Attempt to start binary
run: |
./build/defradb start &
./build/defradb start --no-keyring &
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why do we need to start with this flag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise it will prompt for a password before starting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Might be worth adding instructions for this extra configuration step in the root README.md (perhaps in the configuration section?) where as that is the default, and mention this flag to skip the keyring configuration.

sleep 5

- name: Check if binary is still running
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Read the documentation on [docs.source.network](https://docs.source.network/).
## Table of Contents

- [Install](#install)
- [Key Management](#key-management)
- [Start](#start)
- [Configuration](#configuration)
- [External port binding](#external-port-binding)
Expand Down Expand Up @@ -58,6 +59,33 @@ export PATH=$PATH:$(go env GOPATH)/bin

We recommend experimenting with queries using a native GraphQL client. GraphiQL is a popular option - [download and install it](https://altairgraphql.dev/#download).

## Key Management

DefraDB has a built in keyring that can be used to store private keys securely.

The following keys are loaded from the keyring on start:

- `peer-key` Ed25519 private key (required)
- `encryption-key` AES-128, AES-192, or AES-256 key (optional)

To randomly generate the required keys, run the following command:

```
defradb keyring generate
```

To import externally generated keys, run the following command:

```
defradb keyring import <name> <private-key-hex>
```

To learn more about the available options:

```
defradb keyring --help
```

## Start

Start a node by executing `defradb start`. Keep the node running while going through the following examples.
Expand Down
8 changes: 8 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,17 @@ func NewDefraCommand() *cobra.Command {
collection,
)

keyring := MakeKeyringCommand()
keyring.AddCommand(
MakeKeyringGenerateCommand(),
MakeKeyringImportCommand(),
MakeKeyringExportCommand(),
)

root := MakeRootCommand()
root.AddCommand(
client,
keyring,
MakeStartCommand(),
MakeServerDumpCmd(),
MakeVersionCommand(),
Expand Down
5 changes: 5 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var configPaths = []string{
"datastore.badger.path",
"api.pubkeypath",
"api.privkeypath",
"keyring.path",
}

// configFlags is a mapping of config keys to cli flags to bind to.
Expand All @@ -57,6 +58,10 @@ var configFlags = map[string]string{
"api.allowed-origins": "allowed-origins",
"api.pubkeypath": "pubkeypath",
"api.privkeypath": "privkeypath",
"keyring.namespace": "keyring-namespace",
"keyring.backend": "keyring-backend",
"keyring.path": "keyring-path",
"keyring.disabled": "no-keyring",
}

// defaultConfig returns a new config with default values.
Expand Down
6 changes: 5 additions & 1 deletion cli/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func TestLoadConfigNotExist(t *testing.T) {
require.NoError(t, err)

assert.Equal(t, 5, cfg.GetInt("datastore.maxtxnretries"))

assert.Equal(t, filepath.Join(rootdir, "data"), cfg.GetString("datastore.badger.path"))
assert.Equal(t, 1<<30, cfg.GetInt("datastore.badger.valuelogfilesize"))
assert.Equal(t, "badger", cfg.GetString("datastore.store"))
Expand All @@ -59,4 +58,9 @@ func TestLoadConfigNotExist(t *testing.T) {
assert.Equal(t, false, cfg.GetBool("log.source"))
assert.Equal(t, "", cfg.GetString("log.overrides"))
assert.Equal(t, false, cfg.GetBool("log.nocolor"))

assert.Equal(t, filepath.Join(rootdir, "keys"), cfg.GetString("keyring.path"))
assert.Equal(t, false, cfg.GetBool("keyring.disabled"))
assert.Equal(t, "defradb", cfg.GetString("keyring.namespace"))
assert.Equal(t, "file", cfg.GetString("keyring.backend"))
}
12 changes: 12 additions & 0 deletions cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ import (
"github.com/sourcenetwork/defradb/errors"
)

const errKeyringHelp = `%w

Did you forget to initialize the keyring?

Use the following command to generate the required keys:
defradb keyring generate
`

const (
errInvalidLensConfig string = "invalid lens configuration"
errSchemaVersionNotOfSchema string = "the given schema version is from a different schema"
Expand Down Expand Up @@ -53,3 +61,7 @@ func NewErrSchemaVersionNotOfSchema(schemaRoot string, schemaVersionID string) e
errors.NewKV("SchemaVersionID", schemaVersionID),
)
}

func NewErrKeyringHelp(inner error) error {
return fmt.Errorf(errKeyringHelp, inner)
}
25 changes: 25 additions & 0 deletions cli/keyring.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I wonder if all this logic with generating hashes should be scoped to this cli package.

Maybe it makes sense to extract it in another place so other that other packages can use it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be nice. Would it make sense to have a crypto package?

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"github.com/spf13/cobra"
)

func MakeKeyringCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "keyring",
Short: "Manage DefraDB private keys",
Long: `Manage DefraDB private keys.
Generate, import, and export private keys.`,
}
return cmd
}
41 changes: 41 additions & 0 deletions cli/keyring_export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"github.com/spf13/cobra"
)

func MakeKeyringExportCommand() *cobra.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What are your plans for testing all of this? Are you looking to just rely on manual testing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the file and system keyrings require user input, I don't think they are worth testing. I could add an option for mock (in memory) keyring and add tests for that if you or others think they would be useful.

Copy link
Contributor

@AndrewSisley AndrewSisley May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't need to mock to test it I don't think - tests can handle things like password prompts without to much hassle.

I'm a bit nervous about not pushing for this (either here or in a follow up PR), as apart from the openAPI stuff it would be the only thing in the whole repo that really requires manual testing (and thus is unexpected/surprising, as well as being generally more expensive and unreliable compared to automated testing).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll work on adding some tests. I think I have an idea on how to avoid the user input.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added unit tests for all keyring commands.

var cmd = &cobra.Command{
Use: "export <name>",
Short: "Export a private key",
Long: `Export a private key.
Prints the hexadecimal representation of a private key.

Example:
defradb keyring export encryption-key`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
keyring, err := openKeyring(cmd)
if err != nil {
return err
}
keyBytes, err := keyring.Get(args[0])
if err != nil {
return err
}
cmd.Printf("%x\n", keyBytes)
return nil
},
}
return cmd
}
62 changes: 62 additions & 0 deletions cli/keyring_generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/crypto"
)

func MakeKeyringGenerateCommand() *cobra.Command {
var noEncryption bool
var cmd = &cobra.Command{
Use: "generate",
Short: "Generate private keys",
Long: `Generate private keys.
Randomly generate and store private keys in the keyring.

WARNING: This will overwrite existing keys in the keyring.

Example:
defradb keyring generate

Example: with no encryption key
defradb keyring generate --no-encryption-key

Example: with system keyring
defradb keyring generate --keyring-backend system`,
RunE: func(cmd *cobra.Command, args []string) error {
keyring, err := openKeyring(cmd)
if err != nil {
return err
}
if !noEncryption {
// generate optional encryption key
encryptionKey, err := crypto.GenerateAES256()
if err != nil {
return err
}
err = keyring.Set(encryptionKeyName, encryptionKey)
if err != nil {
return err
}
}
peerKey, err := crypto.GenerateEd25519()
if err != nil {
return err
}
return keyring.Set(peerKeyName, peerKey)
},
}
cmd.Flags().BoolVar(&noEncryption, "no-encryption-key", false, "Skip generating an encryption key")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think it might be worth highlighting that this would disable doc-encryption in the description text.

return cmd
}
42 changes: 42 additions & 0 deletions cli/keyring_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"encoding/hex"

"github.com/spf13/cobra"
)

func MakeKeyringImportCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "import <name> <private-key-hex>",
Short: "Import a private key",
Long: `Import a private key.
Store an externally generated key in the keyring.

Example:
defradb keyring import encryption-key 0000000000000000`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
keyring, err := openKeyring(cmd)
if err != nil {
return err
}
keyBytes, err := hex.DecodeString(args[1])
if err != nil {
return err
}
return keyring.Set(args[0], keyBytes)
},
}
return cmd
}
24 changes: 24 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,29 @@ Start a DefraDB node, interact with a local or remote node, and much more.
"Path to the private key for tls",
)

cmd.PersistentFlags().String(
"keyring-namespace",
"defradb",
"Service name to use when using the system backend",
)

cmd.PersistentFlags().String(
"keyring-backend",
"file",
"Keyring backend to use. Options are file or system",
)

cmd.PersistentFlags().String(
"keyring-path",
"keys",
"Path to store encrypted keys when using the file backend",
)

cmd.PersistentFlags().Bool(
"no-keyring",
false,
Copy link
Member

@shahzadlone shahzadlone Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: By default we want the keyring?

question: For my understanding, what happens to the flow for using keys when there is no keyring? Does it make sense to make keyring functionality (disable or active) dependent on weather acp is on or off?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think enabled by default is a sensible option. The keyring also stores the peer key and encryption key. If you disable the keyring the peer key will be ephemeral and encryption disabled. I don't think the keyring should be dependent on ACP because of the other keys.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, would be nice to document this with my other todo in the README.md

"Disable the keyring and generate ephemeral keys",
Copy link
Contributor

@AndrewSisley AndrewSisley May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Would this disable encryption at rest? If so, I suggest noting that in this description.

question: What happens if keyring commands are called if the keyring is disabled?

question: Is this flag available for all commands? Not just start/init? If so what happens if you disable it whilst/after it has been in use (do you lose access to all data in the node?)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what happens when you use --no-keyring after it has been in use:

defradb start --no-keyring
2024-05-07T14:44:30.085-0400	ERROR	badger	v4@v4.2.1-0.20231113215945-a63444ca5276/db.go:263	Received err: Encryption key mismatch. Cleaning up...
Error: Encryption key mismatch

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What happens if keyring commands are called if the keyring is disabled?

--no-keyring is unused for keyring commands. It won't affect the outcome.

Copy link
Contributor

@AndrewSisley AndrewSisley May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it in start is of less a worry to me than if someone uses it in an active instance, for example:

defradb client collection get --name User bae-123 --no-keyring
// or
defradb client collection create --name User '{ "name": "Bob" }'  --no-keyring

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Would this disable encryption at rest? If so, I suggest noting that in this description.

Yes, I'll update the description.

question: What happens if keyring commands are called if the keyring is disabled?

You can still use the keyring commands to manage keys.

question: Is this flag available for all commands? Not just start/init? If so what happens if you disable it whilst/after it has been in use?

This currently only applies to the start command. I'm planning on moving the flags that only apply to the start command in a future PR.

If you disable the keyring after using it, the peer key will be randomly generated, and at rest encryption will be disabled.

If at rest encryption was previously enabled, the datastore will fail to open due to the encryption key not matching.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--no-keyring is unused for keyring commands. It won't affect the outcome.

It is a no-op? Might it be worth excluding it as a flag from those commands then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently only applies to the start command. I'm planning on moving the flags that only apply to the start command in a future PR.

Okay nice, I'm happy with that - thanks Keenan :)

)

return cmd
}
31 changes: 20 additions & 11 deletions cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/libp2p/go-libp2p/core/peer"
Expand All @@ -23,6 +22,7 @@ import (
"github.com/sourcenetwork/defradb/db"
"github.com/sourcenetwork/defradb/errors"
"github.com/sourcenetwork/defradb/http"
"github.com/sourcenetwork/defradb/keyring"
"github.com/sourcenetwork/defradb/net"
netutils "github.com/sourcenetwork/defradb/net/utils"
"github.com/sourcenetwork/defradb/node"
Expand Down Expand Up @@ -84,23 +84,32 @@ func MakeStartCommand() *cobra.Command {
}

if cfg.GetString("datastore.store") != configStoreMemory {
// It would be ideal to not have the key path tied to the datastore.
// Running with memory store mode will always generate a random key.
// Adding support for an ephemeral mode and moving the key to the
// config would solve both of these issues.
rootDir := mustGetContextRootDir(cmd)
key, err := loadOrGeneratePrivateKey(filepath.Join(rootDir, "data", "key"))
if err != nil {
return err
}
netOpts = append(netOpts, net.WithPrivateKey(key))

// TODO-ACP: Infuture when we add support for the --no-acp flag when admin signatures are in,
// we can allow starting of db without acp. Currently that can only be done programmatically.
// https://github.com/sourcenetwork/defradb/issues/2271
dbOpts = append(dbOpts, db.WithACP(rootDir))
}

if !cfg.GetBool("keyring.disabled") {
kr, err := openKeyring(cmd)
if err != nil {
return NewErrKeyringHelp(err)
}
// load the required peer key
peerKey, err := kr.Get(peerKeyName)
if err != nil {
return NewErrKeyringHelp(err)
}
netOpts = append(netOpts, net.WithPrivateKey(peerKey))
// load the optional encryption key
encryptionKey, err := kr.Get(encryptionKeyName)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
return err
}
storeOpts = append(storeOpts, node.WithEncryptionKey(encryptionKey))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: This implies that if the keyring is enabled, then we also use use at-rest encryption. Should these technically be separate config options?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've added a datastore.encryptionDisabled config item.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I've made the key optional instead of the config option.

}

opts := []node.NodeOpt{
node.WithPeers(peers...),
node.WithStoreOpts(storeOpts...),
Expand Down
Loading
Loading