Skip to content

Commit

Permalink
[CAPPL-213] Workflow keystore for secrets management (#15057)
Browse files Browse the repository at this point in the history
* feat: implement encryptionkey

* feat: implement keystore

* fix: update master mock

* fix: lint

* feat create encryption key if capabilities registry is enabled

* chore: remove debug line

* chore: rename encryption to workflowEncryption

* chore: address name changes

* chore: rename WorkflowEncryption to Workflow
  • Loading branch information
agparadiso authored Nov 5, 2024
1 parent e593ded commit fb4b526
Show file tree
Hide file tree
Showing 9 changed files with 647 additions and 0 deletions.
7 changes: 7 additions & 0 deletions core/cmd/shell_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,13 @@ func (s *Shell) runNode(c *cli.Context) error {
}
}

if s.Config.Capabilities().Peering().Enabled() {
err2 := app.GetKeyStore().Workflow().EnsureKey(rootCtx)
if err2 != nil {
return errors.Wrap(err2, "failed to ensure workflow key")
}
}

err2 := app.GetKeyStore().CSA().EnsureKey(rootCtx)
if err2 != nil {
return errors.Wrap(err2, "failed to ensure CSA key")
Expand Down
44 changes: 44 additions & 0 deletions core/services/keystore/keys/workflowkey/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package workflowkey

import (
"github.com/ethereum/go-ethereum/accounts/keystore"

"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys"
"github.com/smartcontractkit/chainlink/v2/core/utils"
)

const keyTypeIdentifier = "Workflow"

func FromEncryptedJSON(keyJSON []byte, password string) (Key, error) {
return keys.FromEncryptedJSON(
keyTypeIdentifier,
keyJSON,
password,
adulteratedPassword,
func(_ keys.EncryptedKeyExport, rawPrivKey []byte) (Key, error) {
return Raw(rawPrivKey).Key(), nil
},
)
}

func (k Key) ToEncryptedJSON(password string, scryptParams utils.ScryptParams) (export []byte, err error) {
return keys.ToEncryptedJSON(
keyTypeIdentifier,
k.Raw(),
k,
password,
scryptParams,
adulteratedPassword,
func(id string, key Key, cryptoJSON keystore.CryptoJSON) keys.EncryptedKeyExport {
return keys.EncryptedKeyExport{
KeyType: id,
PublicKey: key.PublicKeyString(),
Crypto: cryptoJSON,
}
},
)
}

func adulteratedPassword(password string) string {
return "workflowkey" + password
}
107 changes: 107 additions & 0 deletions core/services/keystore/keys/workflowkey/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package workflowkey

import (
cryptorand "crypto/rand"
"encoding/hex"
"errors"
"fmt"

"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
)

type Raw []byte

func (raw Raw) Key() Key {
privateKey := [32]byte(raw)
return Key{
privateKey: &privateKey,
publicKey: curve25519PubKeyFromPrivateKey(privateKey),
}
}

func (raw Raw) String() string {
return fmt.Sprintf("<%s Raw Private Key>", keyTypeIdentifier)
}

func (raw Raw) GoString() string {
return raw.String()
}

func (raw Raw) Bytes() []byte {
return ([]byte)(raw)
}

type Key struct {
privateKey *[curve25519.PointSize]byte
publicKey *[curve25519.PointSize]byte
}

func New() (Key, error) {
publicKey, privateKey, err := box.GenerateKey(cryptorand.Reader)
if err != nil {
return Key{}, err
}

return Key{
privateKey: privateKey,
publicKey: publicKey,
}, nil
}

func (k Key) PublicKey() [curve25519.PointSize]byte {
return *k.publicKey
}

func (k Key) PublicKeyString() string {
return hex.EncodeToString(k.publicKey[:])
}

func (k Key) ID() string {
return k.PublicKeyString()
}

func (k Key) Raw() Raw {
raw := make([]byte, curve25519.PointSize)
copy(raw, k.privateKey[:])
return Raw(raw)
}

func (k Key) String() string {
return fmt.Sprintf("%sKey{PrivateKey: <redacted>, PublicKey: %s}", keyTypeIdentifier, *k.publicKey)
}

func (k Key) GoString() string {
return k.String()
}

// Encrypt encrypts a message using the public key
func (k Key) Encrypt(plaintext []byte) ([]byte, error) {
publicKey := k.PublicKey()
encrypted, err := box.SealAnonymous(nil, plaintext, &publicKey, cryptorand.Reader)
if err != nil {
return nil, err
}

return encrypted, nil
}

// Decrypt decrypts a message that was encrypted using the private key
func (k Key) Decrypt(ciphertext []byte) (plaintext []byte, err error) {
publicKey := k.PublicKey()
decrypted, success := box.OpenAnonymous(nil, ciphertext, &publicKey, k.privateKey)
if !success {
return nil, errors.New("decryption failed")
}

return decrypted, nil
}

func curve25519PubKeyFromPrivateKey(privateKey [curve25519.PointSize]byte) *[curve25519.PointSize]byte {
var publicKey [curve25519.PointSize]byte

// Derive the public key
curve25519.ScalarBaseMult(&publicKey, &privateKey)

return &publicKey
}
88 changes: 88 additions & 0 deletions core/services/keystore/keys/workflowkey/key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package workflowkey

import (
cryptorand "crypto/rand"
"encoding/hex"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/nacl/box"
)

func TestNew(t *testing.T) {
key, err := New()
require.NoError(t, err)

assert.NotNil(t, key.PublicKey)
assert.NotNil(t, key.privateKey)
}

func TestPublicKey(t *testing.T) {
key, err := New()
require.NoError(t, err)

assert.Equal(t, *key.publicKey, key.PublicKey())
}

func TestEncryptKeyRawPrivateKey(t *testing.T) {
privKey, err := New()
require.NoError(t, err)

privateKey := privKey.Raw()

assert.Equal(t, "<Workflow Raw Private Key>", privateKey.String())
assert.Equal(t, privateKey.String(), privateKey.GoString())
}

func TestEncryptKeyFromRawPrivateKey(t *testing.T) {
boxPubKey, boxPrivKey, err := box.GenerateKey(cryptorand.Reader)
require.NoError(t, err)

privKey := make([]byte, 32)
copy(privKey, boxPrivKey[:])
key := Raw(privKey).Key()

assert.Equal(t, boxPubKey, key.publicKey)
assert.Equal(t, boxPrivKey, key.privateKey)
assert.Equal(t, key.String(), key.GoString())

byteBoxPubKey := make([]byte, 32)
copy(byteBoxPubKey, boxPubKey[:])

assert.Equal(t, hex.EncodeToString(byteBoxPubKey), key.PublicKeyString())
assert.Equal(t, fmt.Sprintf("WorkflowKey{PrivateKey: <redacted>, PublicKey: %s}", byteBoxPubKey), key.String())
}

func TestPublicKeyStringAndID(t *testing.T) {
key := "my-test-public-key"
var pubkey [32]byte
copy(pubkey[:], key)
k := Key{
publicKey: &pubkey,
}

expected := hex.EncodeToString([]byte(key))
// given the key is a [32]byte we need to ensure the encoded string is 64 character long
for len(expected) < 64 {
expected += "0"
}

assert.Equal(t, expected, k.PublicKeyString())
assert.Equal(t, expected, k.ID())
}

func TestDecrypt(t *testing.T) {
key, err := New()
require.NoError(t, err)

secret := []byte("my-secret")
ciphertext, err := key.Encrypt(secret)
require.NoError(t, err)

plaintext, err := key.Decrypt(ciphertext)
require.NoError(t, err)

assert.Equal(t, secret, plaintext)
}
10 changes: 10 additions & 0 deletions core/services/keystore/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/solkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/starkkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/vrfkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/workflowkey"
"github.com/smartcontractkit/chainlink/v2/core/utils"
)

Expand All @@ -45,6 +46,7 @@ type Master interface {
StarkNet() StarkNet
Aptos() Aptos
VRF() VRF
Workflow() Workflow
Unlock(ctx context.Context, password string) error
IsEmpty(ctx context.Context) (bool, error)
}
Expand All @@ -61,6 +63,7 @@ type master struct {
starknet *starknet
aptos *aptos
vrf *vrf
workflow *workflow
}

func New(ds sqlutil.DataSource, scryptParams utils.ScryptParams, lggr logger.Logger) Master {
Expand Down Expand Up @@ -89,6 +92,7 @@ func newMaster(ds sqlutil.DataSource, scryptParams utils.ScryptParams, lggr logg
starknet: newStarkNetKeyStore(km),
aptos: newAptosKeyStore(km),
vrf: newVRFKeyStore(km),
workflow: newWorkflowKeyStore(km),
}
}

Expand Down Expand Up @@ -132,6 +136,10 @@ func (ks *master) VRF() VRF {
return ks.vrf
}

func (ks *master) Workflow() Workflow {
return ks.workflow
}

type ORM interface {
isEmpty(context.Context) (bool, error)
saveEncryptedKeyRing(context.Context, *encryptedKeyRing, ...func(sqlutil.DataSource) error) error
Expand Down Expand Up @@ -267,6 +275,8 @@ func GetFieldNameForKey(unknownKey Key) (string, error) {
return "Aptos", nil
case vrfkey.KeyV2:
return "VRF", nil
case workflowkey.Key:
return "Workflow", nil
}
return "", fmt.Errorf("unknown key type: %T", unknownKey)
}
Expand Down
47 changes: 47 additions & 0 deletions core/services/keystore/mocks/master.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions core/services/keystore/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/solkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/starkkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/vrfkey"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/workflowkey"
"github.com/smartcontractkit/chainlink/v2/core/utils"
)

Expand Down Expand Up @@ -158,6 +159,7 @@ type keyRing struct {
StarkNet map[string]starkkey.Key
Aptos map[string]aptoskey.Key
VRF map[string]vrfkey.KeyV2
Workflow map[string]workflowkey.Key
LegacyKeys LegacyKeyStorage
}

Expand All @@ -173,6 +175,7 @@ func newKeyRing() *keyRing {
StarkNet: make(map[string]starkkey.Key),
Aptos: make(map[string]aptoskey.Key),
VRF: make(map[string]vrfkey.KeyV2),
Workflow: make(map[string]workflowkey.Key),
}
}

Expand Down
Loading

0 comments on commit fb4b526

Please sign in to comment.