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

Add ECC Key Support #993

Merged
merged 20 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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 ee/localserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func New(logger log.Logger, db *bbolt.DB, kolideServer string) (*localServer, er
}

// Consider polling this on an interval, so we get updates.
privateKey, err := osquery.PrivateKeyFromDB(db)
privateKey, err := osquery.PrivateRSAKeyFromDB(db)
if err != nil {
return nil, fmt.Errorf("fetching private key: %w", err)
}
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ require (
github.com/groob/plist v0.0.0-20190114192801-a99fbe489d03
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/knightsc/system_policy v1.1.1-0.20211029142728-5f4c0d5419cc
github.com/kolide/kit v0.0.0-20220920212810-17eca5d2e6d2
github.com/kolide/krypto v0.0.0-20220830180245-7cb3a3940071
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab
github.com/kolide/krypto v0.0.0-20230123214238-2219d22c9a17
github.com/kolide/updater v0.0.0-20190315001611-15bbc19b5b80
github.com/kr/pty v1.1.2
github.com/mat/besticon v3.9.0+incompatible
Expand All @@ -40,7 +40,7 @@ require (
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.etcd.io/bbolt v1.3.6
go.opencensus.io v0.23.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/crypto v0.4.0
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
golang.org/x/image v0.3.0
golang.org/x/net v0.4.0
Expand Down Expand Up @@ -74,6 +74,7 @@ require (
github.com/go-logfmt/logfmt v0.4.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/certificate-transparency-go v1.0.21 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
Expand Down
70 changes: 65 additions & 5 deletions go.sum

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions pkg/agent/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package agent

import (
"crypto"
"fmt"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/launcher/pkg/agent/keys"
"go.etcd.io/bbolt"
)

type keyInt interface {
crypto.Signer
//Type() string // Not Yet Supported by Krypto
}

var Keys keyInt = keys.Noop
var LocalDbKeys keyInt = keys.Noop

func SetupKeys(logger log.Logger, db *bbolt.DB) error {
var err error

// Always setup a local key
LocalDbKeys, err = keys.SetupLocalDbKey(logger, db)
if err != nil {
return fmt.Errorf("setting up local db keys: %w", err)
}

Keys, err = setupHardwareKeys(logger, db)
if err != nil {
// Now this is a conundrum. What should we do if there's a hardware keying error?
// We could return the error, and abort, but that would block launcher for working in places
// without keys. Inatead, we log the error and set Keys to the localDb key.
level.Info(logger).Log("msg", "setting up hardware keys", "err", err)
Keys = LocalDbKeys
}

return nil
}

// This duplicates some of pkg/osquery/extension.go but that feels like the wrong place.
// Really, we should have a simpler interface over a storage layer.
const (
bucketName = "config"
privateEccData = "privateEccData"
publicEccData = "publicEccData"
)

func fetchKeyData(db *bbolt.DB) ([]byte, []byte, error) {
var pri []byte
var pub []byte

if err := db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucketName))
if b == nil {
return nil
}

pri = b.Get([]byte(privateEccData))
pub = b.Get([]byte(publicEccData))

return nil
}); err != nil {
return nil, nil, err
}

return pri, pub, nil
}

func storeKeyData(db *bbolt.DB, pri, pub []byte) error {
return db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return fmt.Errorf("creating bucket: %w", err)
}

if err := b.Put([]byte(privateEccData), pri); err != nil {
return err
}

if err := b.Put([]byte(publicEccData), pub); err != nil {
return err
}

return nil
})
}

// clearKeyData is used to clear the keys as part of error handling around new keys. It is not intended to be called
// regularly, and since the path that calls it is around DB errors, it has no error handling.
func clearKeyData(logger log.Logger, db *bbolt.DB) {
level.Info(logger).Log("msg", "Clearing keys")
_ = db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucketName))
if b == nil {
return nil
}

_ = b.Delete([]byte(privateEccData))
_ = b.Delete([]byte(publicEccData))

return nil
})
}
94 changes: 94 additions & 0 deletions pkg/agent/keys/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package keys

import (
"crypto/ecdsa"
"crypto/x509"
"fmt"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/krypto/pkg/echelper"
"go.etcd.io/bbolt"
)

// This duplicates some of pkg/osquery/extension.go but that feels like the wrong place.
// Really, we should have a simpler interface over a storage layer.
const (
bucketName = "config"
localKey = "localEccKey"
)

// dbKey is keyInt over a key stored in the agent database. Its used in places where we don't want, or don't have, the hardware key.
type dbKey struct {
*ecdsa.PrivateKey
}

func (k dbKey) Type() string {
return "local"
}

func SetupLocalDbKey(logger log.Logger, db *bbolt.DB) (*dbKey, error) {
if key, err := fetchKey(db); key != nil && err == nil {
level.Info(logger).Log("msg", "found local key in database")
return &dbKey{key}, nil
} else if err != nil {
level.Info(logger).Log("msg", "Failed to parse key, regenerating", "err", err)
} else if key == nil {
level.Info(logger).Log("msg", "No key found, generating new key")
}

// Time to regenerate!
key, err := echelper.GenerateEcdsaKey()
if err != nil {
return nil, fmt.Errorf("generating new key: %w", err)
}

// Store the key in the database.
if err := storeKey(db, key); err != nil {
return nil, fmt.Errorf("storing new key: %w", err)
}

return &dbKey{key}, nil
}

func fetchKey(db *bbolt.DB) (*ecdsa.PrivateKey, error) {
var raw []byte

// There's nothing that can really return an error here. Either we have a key, or we don't.
_ = db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucketName))
if b == nil {
return nil
}

raw = b.Get([]byte(localKey))
return nil
})

// No key, just return nils
if raw == nil {
return nil, nil
}

return x509.ParseECPrivateKey(raw)
}

func storeKey(db *bbolt.DB, key *ecdsa.PrivateKey) error {
raw, err := x509.MarshalECPrivateKey(key)
if err != nil {
return fmt.Errorf("marshaling key: %w", err)
}

return db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return fmt.Errorf("creating bucket: %w", err)
}

if err := b.Put([]byte(localKey), raw); err != nil {
return err
}

return nil
})
}
45 changes: 45 additions & 0 deletions pkg/agent/keys/local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package keys

import (
"os"
"path/filepath"
"testing"

"github.com/go-kit/kit/log"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
)

func TestSetupLocalDbKey(t *testing.T) {
t.Parallel()

db := setupDb(t)
logger := log.NewJSONLogger(os.Stderr) //log.NewNopLogger()

key, err := SetupLocalDbKey(logger, db)
require.NoError(t, err)
require.NotNil(t, key)

// Call a thing. Make sure this is a real key
require.NotNil(t, key.Public())

// If we call this _again_ do we get the same key back?
key2, err := SetupLocalDbKey(logger, db)
require.NoError(t, err)
require.Equal(t, key.Public(), key2.Public())

}

func setupDb(t *testing.T) *bbolt.DB {
// Create a temp directory to hold our bbolt db
dbDir := t.TempDir()

// Create database; ensure we clean it up after the test
db, err := bbolt.Open(filepath.Join(dbDir, "test.db"), 0600, nil)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, db.Close())
})

return db
}
25 changes: 25 additions & 0 deletions pkg/agent/keys/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package keys

import (
"crypto"
"errors"
"io"
)

// noopKeys is a no-op implementation of keyInt. It's here to be a default
type noopKeys struct {
}

func (n noopKeys) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) (signature []byte, err error) {
return nil, errors.New("Can't sign. Unconfigured keys")
}

func (n noopKeys) Public() crypto.PublicKey {
return nil
}

func (n noopKeys) Type() string {
return "noop"
}

var Noop = noopKeys{}
42 changes: 42 additions & 0 deletions pkg/agent/keys_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build darwin
// +build darwin

package agent

import (
"fmt"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/krypto/pkg/secureenclave"
"go.etcd.io/bbolt"
)

func setupHardwareKeys(logger log.Logger, db *bbolt.DB) (keyInt, error) {
_, pubData, err := fetchKeyData(db)
if err != nil {
return nil, err
}

if pubData == nil {
level.Info(logger).Log("Generating new keys")

var err error
pubData, err = secureenclave.CreateKey()
if err != nil {
return nil, fmt.Errorf("creating key: %w", err)
}

if err := storeKeyData(db, nil, pubData); err != nil {
clearKeyData(logger, db)
return nil, fmt.Errorf("storing key: %w", err)
}
}

k, err := secureenclave.New(pubData)
if err != nil {
return nil, fmt.Errorf("creating secureenclave signer: %w", err)
}

return k, nil
}
43 changes: 43 additions & 0 deletions pkg/agent/keys_tpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//go:build !darwin
// +build !darwin

package agent

import (
"fmt"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/krypto/pkg/tpm"
"go.etcd.io/bbolt"
)

func setupHardwareKeys(logger log.Logger, db *bbolt.DB) (keyInt, error) {
priData, pubData, err := fetchKeyData(db)
if err != nil {
return nil, err
}

if pubData == nil || priData == nil {
level.Info(logger).Log("Generating new keys")

var err error
priData, pubData, err = tpm.CreateKey()
if err != nil {
clearKeyData(logger, db)
return nil, fmt.Errorf("creating key: %w", err)
}

if err := storeKeyData(db, priData, pubData); err != nil {
clearKeyData(logger, db)
return nil, fmt.Errorf("storing key: %w", err)
}
}

k, err := tpm.New(priData, pubData)
if err != nil {
return nil, fmt.Errorf("creating tpm signer:, from new key %w", err)
}

return k, nil
}
Loading